Documentation
¶
Index ¶
- Variables
- func ComputeOverage(tiers []plan.PriceTier, usage, included int64, currency string) (types.Money, error)
- func SortTiers(tiers []plan.PriceTier) []plan.PriceTier
- func ValidateTiers(tiers []plan.PriceTier, currency string) error
- type Invoice
- type LineItem
- type LineItemType
- type ListOpts
- type OverdueOpts
- type Status
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidTiers = errors.New("invoice: invalid price tiers")
ErrInvalidTiers is wrapped by every ValidateTiers failure.
Functions ¶
func ComputeOverage ¶
func ComputeOverage(tiers []plan.PriceTier, usage, included int64, currency string) (types.Money, error)
ComputeOverage prices the billable excess of usage over an included allowance, using whichever tier model the plan declares.
currency is normalised to lowercase once, here, at entry. Every Money this function and its helpers construct downstream uses that normalised value, so a caller passing "USD" gets an amount back in "usd" rather than hitting the currency-mismatch panic in types.Money.Add/Subtract.
UpTo counts TOTAL period usage, not units past the allowance: a plan that prices its allowance as a $0 first tier (as this package's README example does) needs `usage`, not `usage - included`, to land in the right tier, or the allowance would be priced twice. Concretely:
- graduated: price(usage) - price(included). Each unit is charged at the rate of the tier position it occupies in the ladder; billing only the delta between the two totals prices the included units once, at graduated's per-position rate, not again on top of it.
- volume: the tier reached by TOTAL usage sets the rate, applied to usage - included units.
- flat: max(0, fee(usage) - fee(included)), where fee(q) is the FlatAmount of the tier reached by total quantity q. Charging the whole band fee for any usage past included would re-bill the band the allowance already sat in: included 3000 with usage 3001 must not bill a fresh fee for one unit inside a band already paid for.
A ladder with no unbounded tier does not stop pricing at its highest UpTo: the last tier after sorting extends to cover everything above it (see computeGraduated, computeVolume, computeFlat, and tierAt).
Every product and sum is checked. When one overflows an int64 the result is an error wrapping types.ErrOverflow and a zero Money, never a wrapped amount: a plugin aggregator reporting MaxInt64/2 units at 3c used to price as a huge negative overage.
included < 0 means an unlimited allowance, matching plan.Feature.Limit == -1, and prices at zero regardless of usage. usage <= included also prices at zero.
An empty tier slice prices at zero: a metered feature whose plan declares no tiers is a gap in the catalogue, not a billing failure. Callers must call ValidateTiers before ComputeOverage, which reports only arithmetic overflow and prices an invalid ladder by best effort; the default branch below, for a tier type ValidateTiers would have rejected, also prices at zero, but ValidateTiers is what is meant to keep callers from reaching it.
func SortTiers ¶
SortTiers returns tiers ordered for evaluation: by UpTo ascending, with any UpTo <= 0 (0 in this package's own tests, -1 in the documented schema) treated as unbounded and sorted last, and Priority breaking ties.
The input is not mutated. Callers hold tiers that belong to a stored plan, and reordering that slice in place would rewrite the plan's own view of its pricing.
func ValidateTiers ¶
ValidateTiers reports whether one feature's tiers can be priced unambiguously in the given currency. Callers must validate before ComputeOverage, which prices an invalid ladder by best effort and reports only arithmetic overflow.
Every returned error names the offending tier by its index in the slice and its UpTo, not only its FeatureKey: a plan with several tiers sharing one feature key would otherwise leave the caller guessing which tier is bad.
An empty slice is valid: a metered feature with no tiers yet is a catalogue gap for ComputeOverage to price at zero, not a validation failure. Otherwise ValidateTiers rejects a ladder where:
- the tiers do not all share one Type;
- any Type is empty or not one of graduated, volume, or flat;
- any tier's UnitAmount.Currency or FlatAmount.Currency is non-empty and differs from currency, case-insensitively;
- the tiers carry more than one distinct FeatureKey;
- two tiers have the same UpTo, treating every UpTo <= 0 as the same unbounded value (so a 0 and a -1 together are a duplicate);
- a graduated or volume tier has a non-zero FlatAmount, or a flat tier has a non-zero UnitAmount;
- any tier's UnitAmount.Amount or FlatAmount.Amount is negative. A negative rate would quietly reduce an invoice instead of erroring.
Types ¶
type Invoice ¶
type Invoice struct {
types.Entity
ID id.InvoiceID `json:"id"`
TenantID string `json:"tenant_id"`
SubscriptionID id.SubscriptionID `json:"subscription_id"`
Status Status `json:"status"`
Currency string `json:"currency"`
Subtotal types.Money `json:"subtotal"`
TaxAmount types.Money `json:"tax_amount"`
DiscountAmount types.Money `json:"discount_amount"`
CreditAmount types.Money `json:"credit_amount"`
Total types.Money `json:"total"`
LineItems []LineItem `json:"line_items"`
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
DueDate *time.Time `json:"due_date,omitempty"`
PaidAt *time.Time `json:"paid_at,omitempty"`
VoidedAt *time.Time `json:"voided_at,omitempty"`
VoidReason string `json:"void_reason,omitempty"`
PaymentRef string `json:"payment_ref,omitempty"`
ProviderID string `json:"provider_id,omitempty"`
ProviderName string `json:"provider_name,omitempty"`
AppID string `json:"app_id"`
Metadata map[string]string `json:"metadata,omitempty"`
}
Invoice is a bill for one subscription period.
LineItems sum to Subtotal, minus CreditAmount and DiscountAmount, plus TaxAmount. That sum equals Total, except when the credit and discount exceed the subtotal: the net amount then clamps at zero before tax is added, rather than letting Total go negative. CreditAmount is the plan's credit pools applied to usage; DiscountAmount is coupons.
type LineItem ¶
type LineItem struct {
ID id.LineItemID `json:"id"`
InvoiceID id.InvoiceID `json:"invoice_id"`
FeatureKey string `json:"feature_key,omitempty"`
Description string `json:"description"`
Quantity int64 `json:"quantity"`
UnitAmount types.Money `json:"unit_amount"`
Amount types.Money `json:"amount"`
Type LineItemType `json:"type"`
Metadata map[string]string `json:"metadata,omitempty"`
}
LineItem is one priced component of an Invoice.
A discount or credit line item carries a negative Amount (and UnitAmount), while the invoice's own DiscountAmount and CreditAmount are always recorded positive. A line item priced from a tier ladder, such as usage overage or seat charges, carries a zero UnitAmount: a ladder has no single per-unit price, so only Amount, the total charged, means anything for it.
type LineItemType ¶
type LineItemType string
const ( LineItemBase LineItemType = "base" LineItemUsage LineItemType = "usage" LineItemOverage LineItemType = "overage" LineItemSeat LineItemType = "seat" LineItemCredit LineItemType = "credit" LineItemDiscount LineItemType = "discount" LineItemTax LineItemType = "tax" )
type OverdueOpts ¶
OverdueOpts selects pending invoices whose due date is before Before, earliest due date first, ties broken by id. An empty AppID means every app.
type Store ¶
type Store interface {
Create(ctx context.Context, inv *Invoice) error
Get(ctx context.Context, invID id.InvoiceID) (*Invoice, error)
List(ctx context.Context, tenantID, appID string, opts ListOpts) ([]*Invoice, error)
Update(ctx context.Context, inv *Invoice) error
GetByPeriod(ctx context.Context, tenantID, appID string, periodStart, periodEnd time.Time) (*Invoice, error)
ListPending(ctx context.Context, appID string) ([]*Invoice, error)
MarkPaid(ctx context.Context, invID id.InvoiceID, paidAt time.Time, paymentRef string) error
MarkVoided(ctx context.Context, invID id.InvoiceID, reason string) error
}