# Billing Config & Support Fee Architecture

## Billing Config Types

All partner relationships in this codebase are **1:N** — one partner tenant has many customer tenants. What differs across the three partner modes is **where the support plan is bound** and **how the partner's books are partitioned**:

| Type | Where is support bound? | Partner BC structure | Customer ↔ Partner link |
|------|-------------------------|----------------------|-------------------------|
| `MSPCustomer` / `MSP` | **Partner** | 1 partner BC | MSP partner has 1 BC; each customer has 1 BC. Support plan is bound on the **partner** (see `MspCustomerSyncSupportPlan`), so the support fee lives on the partner's books. Customer-side `user_unit_costs` is irrelevant for the fee. |
| `MarketplaceResellerManagedCustomer` / `MarketplaceReseller` (CPPO) | **Customer** | **N partner BCs** (one per customer) | Each customer and its partner BC are created atomically via `CreateMarketplaceResellerBillingConfig`. The pair is linked via `billing_configs.ManagedTenantID = customerTenantID`. Per-customer partner BC mirrors the customer, so reading the partner BC is equivalent to reading the customer BC. |
| `ResaleCustomer` / `ResalePartner` | **Customer** | **1 partner BC** (shared by all customers) | Link is via `payment_methods.PaymentCode` set by `ResaleHandlerImpl.Setup` at onboarding. The single partner BC means the partner's `user_unit_costs` aggregates all customers' costs — the support fee must therefore be computed per-customer and attributed via the `MarketplaceResellerManagedCustomerIDLabel`. |
| `DirectCustomer` | Customer (self) | n/a | No partner push. |

> **Common misconception**: "CPPO is 1:1." The relationship is 1:N at the partner-tenant level (a partner can manage many customers). What is 1:1 for CPPO is the **billing-config pair**: each customer has exactly one dedicated partner BC. That is why the per-customer base works for both CPPO and Resale despite their different partner-BC structure — the **source** of the support-fee base is always the customer's own usage, never the partner's aggregate.

## Support Fee Calculation

### Trigger

Support plan metering is triggered by `workers.SupportPlanWorkerByTenant(tenantID)`, which runs on every tenant that has an active or expired bound support plan:

1. `SendNormalMetering` — daily support fee written to the customer's own billing config
2. `SendResellerNormalMeteringIfNeed` — daily support fee pushed to the partner's billing config (only for `ResaleCustomer` and `MarketplaceResellerManagedCustomer`)
3. `SendCommitmentMetering` — monthly commitment gap written to the customer's own billing config
4. `SendResellerCommitmentMeteringIfNeed` — monthly commitment gap pushed to the partner's billing config (same condition)

**The trigger always runs on the customer's worker.** The partner does not independently trigger support-fee calculation for its managed customers (except in MSP, where the trigger runs on the partner directly because the support plan is bound there — see `MspCustomerSyncSupportPlan`).

### Source/Target Split (CPPO and Resale only)

The core refactoring introduces an explicit source-vs-target split:

- **Source** (`sourceTenantID`, `sourceBillingConfigID`): The customer — where the actual usage and support-fee spend lives. `getCustomerUsageBase` and `getCustomerSupportFeeSpend` read from this.
- **Target** (`targetTenantID`, `targetBillingConfigID`): The partner — where the support-fee row is written. `SecondaryMeasure` writes to this.

```go
// Normal metering
sendToBillNormalMetering(costID, measureDate, sourceTenantID, sourceBillingConfigID, targetTenantID, targetBillingConfigID, bindedSupportPlanInfo, isMarketplaceReseller)

// Commitment metering
sendToBillCommitmentMetering(costID, sourceTenantID, sourceBillingConfigID, targetTenantID, targetBillingConfigID, startTime, endTime, measureDay, bindedSupportPlanInfo, isMarketplaceReseller)
```

| Mode | source | target | Why |
|------|--------|--------|-----|
| `SendNormalMetering` (any partner-managed customer, or `DirectCustomer`) | customer (same as bind plan tenant) | customer (same tenant) | daily fee lives on the customer's own books |
| CPPO `SendReseller*` | customer | customer's dedicated `MarketplaceReseller` BC | per-customer partner BC mirrors the customer's usage, so source and target hold the same aggregate for fee purposes |
| Resale `SendReseller*` | customer | shared `ResalePartner` BC | partner's books aggregate all N customers; per-customer base must be read from each customer's own BC and attributed via `MarketplaceResellerManagedCustomerIDLabel` |
| MSP | partner | partner | `SendReseller*` is never called for MSPCustomer; the support plan is bound on the partner and `SendNormalMetering` reads the partner's own `user_unit_costs` (source==target==partner) |

### Key Helper Functions

**`isPartnerManagedCustomer(type string) bool`** (`support_plan.go`)

Returns `true` for billing-config types whose support fee should be pushed via `SendReseller*`:

- `MarketplaceResellerManagedCustomer` (CPPO) — pushed to per-customer `MarketplaceReseller` BC
- `ResaleCustomer` — pushed to shared `ResalePartner` BC

Intentionally excludes `MSPCustomer`. MSP partners bind support plans on their own tenant (`MspCustomerSyncSupportPlan`); the cost is computed from the partner's `user_unit_costs`, not the customer's. `SendReseller*` is never invoked for MSPCustomer billing configs.

**`getResellerTenantAndBillingConfig(customerTenantID, customerBillingConfig)`** (`support_plan.go`)

Resolves the partner tenant and billing config for a given customer:

- `MarketplaceResellerManagedCustomer`: reads `billing_configs.ManagedTenantID = customerTenantID`, returns the matching `MarketplaceReseller` BC
- `ResaleCustomer`: reads the customer's active `Resale` payment method, parses `PaymentCode` as partner tenant ID, then loads the `ResalePartner` BC

**`getCustomerUsageBase(customerTenantID, customerBillingConfigID, measureDate)`** (`support_plan.go`)

Sums customer `CostPrice` for a single day, **excluding** the four support SKUs (`FixedPrice_SKU`, `PercentagePrice_SKU`, `TieredPrice_SKU`, `CommintmentPrice_SKU`). The exclusion is critical: without it, `SendNormal`'s own prior-day row would be read back as "usage" and inflate the next day's base (circular reference).

**`getCustomerSupportFeeSpend(customerTenantID, customerBillingConfigID, startTime, endTime)`** (`support_plan.go`)

Sums customer `CostPrice` for the support-fee SKUs over a time range, **excluding** `CommintmentPrice_SKU`. Used by commitment metering to compute the gap (commitment minus actual spend). The exclusion prevents the commitment row from being read back into its own calculation.

### Support Fee Types

| Type | SKU | Calculation |
|------|-----|-------------|
| Fixed Price | `support-plan:fixed_price` | `FixedChargePrice / 30 * 1000` per day (constant) |
| Percentage Price | `support-plan:percentage_price` | `dayCostPrice * 1000 * SupportPricePercentage` |
| Tiered Price | `support-plan:tiered_price` | Tiered pricing on cumulative usage, read from `SupportPlanMeteringRecords` on the target tenant |
| Commitment Price | `support-plan:commitment_price` | `max(0, commitment - actualSupportFeeSpend) * 1000` per month |

### Circular Reference Prevention

Two circular-reference protections are in place:

1. **Normal metering**: `getCustomerUsageBase` excludes all four support SKUs from the daily usage base. Without this, day N would read its own day N-1 support-fee row as "usage" and inflate the next day's base.

2. **Commitment metering**: `getCustomerSupportFeeSpend` excludes `CommintmentPrice_SKU` from the spend base. Without this, the commitment row (written by this function) would be read back into the gap calculation on the next call.

## Payment Handler Integration

### Support Fee Skip During Pay Transfer

When a partner's invoice is paid (e.g., during `MspHandlerImpl.Pay`), the support-fee units must **not** be re-transferred from the customer to the partner — they were already pushed directly by the support plan worker's `SendReseller*` helpers. Re-transferring would double-count.

`MspHandlerImpl.Pay` (`msp.go`) skips these four units for any partner-managed customer's costs (the function is invoked for both `MSPCustomer` and `ResaleCustomer` invoices — `ResaleHandlerImpl` embeds `MspHandlerImpl`):
```go
case constant.FixedPrice_SKU,
    constant.PercentagePrice_SKU,
    constant.CommintmentPrice_SKU,
    constant.TieredPrice_SKU:
    return nil // skip — already pushed by SendReseller* worker (or, for MSP, already on partner books)
```

`ResaleHandlerImpl` embeds `MspHandlerImpl` (`resale.go`), so this guard applies to the Resale path too. For Resale it is a defensive no-op (the support fee is never on the customer's invoice, only on the partner's), but it must stay because `MspHandlerImpl.Pay` is shared with the MSP path where the double-counting risk is real.

### Label Attribution

Support-fee rows written to a shared partner BC (`ResalePartner`) carry the `MarketplaceResellerManagedCustomerIDLabel` (`"MSPCustomerID"`) label set to the source customer's tenant ID. This lets the partner attribute each support-fee line back to the originating customer. Set in both `sendToBillNormalMetering` and `sendToBillCommitmentMetering` when `isMarketplaceReseller=true`.

For CPPO, the per-customer partner BC makes the label redundant — each BC has exactly one customer — but the label is set anyway for consistency.

## Test Coverage

| Scenario | DirectCustomer | CPPO 1:1 | Resale 1:N |
|---|---|---|---|
| Normal metering (fixed) | ✓ | — | — |
| Normal metering (percentage) | ✓ | ✓ | ✓ |
| Normal metering (tiered) | — | ✓ | ✓ |
| Commitment metering | ✓ | ✓ | ✓ |
| Label attribution | — | — | ✓ |
| Payment integration | — | ✓ | — |

See `model/marketing/tests/support_plan_test.go`:
- `TestSendResaleOneToNManyCustomers` — percentage-price, label verification
- `TestSendResaleOneToNCommitmentMetering` — commitment gap, idempotency
- `TestSendResaleOneToNTieredPricing` — cumulative usage tracking, per-customer tier advancement

## Related Files

| File | Role |
|------|------|
| `model/marketing/support_plan.go` | Support plan model: metering, source/target split, helper functions |
| `workers/support_plan_worker.go` | Worker trigger: `SendSupportPlanMeteringByTenant` |
| `handler/paymenthandler/impl/msp.go` | MSP Pay handler: support-fee skip during transfer |
| `handler/paymenthandler/impl/resale.go` | Resale handler: embeds MSP handler, overrides Setup |
| `handler/paymenthandler/impl/aws_cppo.go` | CPPO handler: original implementation of the same pattern |