Documentation
¶
Overview ¶
Package tariff is a pure-Go usage-billing rating core: a price-plan component plus a usage quantity in, itemized line items out, with exact, deterministically-rounded amounts. It has zero dependencies — the standard library's math/big is the exact-rate engine.
tariff rates; it does not meter, store, tax, or invoice. It takes a quantity already aggregated by whatever the caller uses and turns it into money.
c := tariff.Charge{
Model: tariff.Graduated,
Currency: tariff.USD(tariff.RoundHalfUp),
Tiers: []tariff.Tier{
{UpTo: 5, UnitRate: big.NewRat(7, 1)},
{UpTo: 10, UnitRate: big.NewRat(13, 2)}, // $6.50
{Last: true, UnitRate: big.NewRat(6, 1)},
},
}
res, _ := c.Rate(6)
fmt.Println(c.Currency.Format(res.Total)) // 41.50
The exactness discipline ¶
Rates are math/big.Rat, exact rationals: a per-unit price of $0.0006 is exactly 6/10000, not a float. quantity * rate is evaluated entirely in math/big and is never a float64. The exact amount is rounded to a whole minor unit exactly once, at the line boundary, using a caller-selected rounding mode — half-up, half-even (banker's), floor, or ceil. There is no hidden default: a Currency whose RoundingMode is unset is refused, because a silent default is a compliance bug.
Amounts out are int64 counts of the currency's minor unit. The scale is currency-driven — 2 decimals for USD, 0 for JPY, 3 for KWD — and never hardcoded to cents. tariff ships no money type; the caller wraps the int64 amounts in whatever they like at the boundary.
Rating models ¶
See Model. PerUnit is quantity times a flat rate. Graduated charges each tier's units at that tier's rate and sums them (marginal). Volume charges the whole quantity at the single rate of the tier it lands in — under a decreasing schedule the total can fall as usage grows, which is intended. Package rounds up to whole blocks after a free allowance. Stairstep charges a flat fee per tier band. A free allowance and an optional fixed flat fee compose with every model.
Tiers are half-open on the low side: tier i covers the units in (tiers[i-1].UpTo, tiers[i].UpTo], the first covering (0, tiers[0].UpTo], and the final tier is unbounded.
Line-item reconciliation ¶
A graduated charge rates every tier exactly, sums exactly, rounds the total once, then allocates that rounded total back across the tier lines with Allocate, so the line subtotals sum to the total with no drift. Rounding each line independently would let three lines that each end in half a minor unit sum to one unit more than the correctly rounded total; allocation prevents that. Across any Result, the line subtotals sum to Total exactly.
Allocation ¶
Allocate splits an amount across parts by ratio, distributing the floor of each share and handing the leftover minor units to the parts with the largest fractional remainder (the largest-remainder, or Hamilton, method). It loses nothing, is deterministic, and never credits a penny to a part that did not round up — a zero ratio receives zero. The total may be negative, so a proration credit splits across lines with the sign carried through. This is the property that makes reconciliation and proration penny-safe.
Proration ¶
A subscription priced per billing period that changes mid-period is rated with the verified cross-vendor model — credit the unused old price, charge the new price for the remaining time, and net them — not a true-forward. A Period is a half-open instant range [Start, End) anchored in a *time.Location; Period.Fraction returns the exact fraction of it covered by a window as a math/big.Rat, under a Basis of ProrateBySecond (real elapsed time, DST-correct) or ProrateByDay (whole civil days in the period's location, so a 23- or 25-hour DST day counts as exactly one). Prorate scales an amount by a fraction, rounded once through the currency; Change returns a Proration of Credit (negative), Charge and Net for a plan change, so oldAmount = 0 is a trial-to-paid change with a zero credit.
Cycle boundaries come from NextBoundary (anniversary cycles, N units from an anchor) and NextCalendarBoundary (calendar-aligned, anchored on the 1st), over a CycleUnit of Monthly or Yearly. The month-end case is drift-free: a boundary's day is the anchor's day clamped to the target month's last valid day, so a January 31 anchor steps to February 28/29 and back to March 31.
Composition ¶
Charges, discounts, minimums, prepaid credits and spend commitments combine in a caller-controlled order, because that order is where real systems disagree — a percentage discount before or after a minimum yields different totals — and tariff refuses to bake one in. Compose left-folds a sealed set of Step values over an empty Invoice in the given order: Charged, PercentOff, AmountOff, MinimumCharge, DrawCredit and DrawCommitment. Each step appends a labeled, auditable Line and moves the running total by exactly that amount, so the invoice lines always reconcile to Total and the sequence that produced them is visible.
Errors ¶
Failures are typed sentinels matchable with errors.Is: ErrNegativeQuantity, ErrEmptyTiers, ErrTierOrder, ErrNoRate, ErrBadPackage, ErrBadAllowance, ErrBadCurrency, ErrBadAllocation, ErrOverflow, ErrUnknownModel, ErrBadPeriod, ErrBadWindow, ErrBadBasis, ErrBadDiscount, ErrBadFloor, ErrBadBalance, ErrCurrencyMismatch and ErrNilStep.
Deviations from the design sketch ¶
Two intentional refinements over the indicative shape in docs/DESIGN.md, recorded there under "Phase 1 as built":
- Charge gains an optional FlatFee (minor units) so a fixed-plus-usage charge — the most common SaaS shape — is one charge, and so the vendor "$49 = 65000 * $0.0006 + $10" vector is reproduced exactly. It emits its own line and applies even at zero usage.
- Rounding is explicit on the Currency (no default), surfaced as USD, JPY and KWD constructors that force the choice.
The design's parenthetical that a volume total decreases from quantity 10 to 11 under the Stripe golden schedule is inaccurate — there it rises from $65.00 to $66.00. The decrease property is real but needs a steeper rate drop; see the volume tests. Likewise the correct graduated total for quantity 11 under that schedule is $73.50 (5*$7 + 5*$6.50 + 1*$6), not $71.50.
Example ¶
Example rates six units on a graduated schedule: 1-5 @ $7, 6-10 @ $6.50, 11+ @ $6.
package main
import (
"fmt"
"math/big"
"github.com/zkrebbekx/tariff"
)
func main() {
c := tariff.Charge{
Model: tariff.Graduated,
Currency: tariff.USD(tariff.RoundHalfUp),
Tiers: []tariff.Tier{
{UpTo: 5, UnitRate: big.NewRat(7, 1)},
{UpTo: 10, UnitRate: big.NewRat(13, 2)}, // $6.50
{Last: true, UnitRate: big.NewRat(6, 1)},
},
}
res, _ := c.Rate(6)
fmt.Printf("total %s across %d lines\n", c.Currency.Format(res.Total), len(res.Lines))
}
Output: total 41.50 across 2 lines
Example (Allocate) ¶
Example_allocate splits a rounded total across parts by ratio, losing nothing: with equal ratios the leftover cent goes to the first part.
package main
import (
"fmt"
"github.com/zkrebbekx/tariff"
)
func main() {
shares, _ := tariff.Allocate(100, []int64{1, 1, 1})
fmt.Println(shares)
}
Output: [34 33 33]
Example (Compose) ¶
Example_compose shows that the order of steps is the caller's and is visible in the result: discounting a $100 charge by 10% then applying a $95 minimum floors the $90 back up to $95.
package main
import (
"fmt"
"math/big"
"github.com/zkrebbekx/tariff"
)
func main() {
usd := tariff.USD(tariff.RoundHalfUp)
c := tariff.Charge{Model: tariff.PerUnit, Currency: usd, UnitRate: big.NewRat(100, 1)}
inv, _ := tariff.Compose(usd,
tariff.Charged(c, 1),
tariff.PercentOff(big.NewRat(1, 10), "10% off"),
tariff.MinimumCharge(9500, "minimum $95"),
)
fmt.Println(usd.Format(inv.Total))
}
Output: 95.00
Example (Package) ¶
Example_package rounds the chargeable quantity up to whole blocks after the free allowance: 201 - 100 free = 101 units, which is two $5 blocks.
package main
import (
"fmt"
"github.com/zkrebbekx/tariff"
)
func main() {
c := tariff.Charge{
Model: tariff.Package,
Currency: tariff.USD(tariff.RoundHalfUp),
PackageSize: 100,
PackagePrice: 500, // $5.00
FreeAllowance: 100,
}
res, _ := c.Rate(201)
fmt.Println(c.Currency.Format(res.Total))
}
Output: 10.00
Example (Proration) ¶
Example_proration reproduces the Stripe mid-period upgrade: a $10 plan upgraded to $20 exactly halfway through a period is credited $5 for the unused old plan and charged $10 for the remaining new plan, netting $5.
package main
import (
"fmt"
"time"
"github.com/zkrebbekx/tariff"
)
func main() {
usd := tariff.USD(tariff.RoundHalfUp)
p := tariff.Period{
Start: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2026, time.January, 31, 0, 0, 0, 0, time.UTC),
}
at := time.Date(2026, time.January, 16, 0, 0, 0, 0, time.UTC)
pr, _ := tariff.Change(1000, 2000, usd, p, at, tariff.ProrateBySecond)
fmt.Printf("credit %s, charge %s, net %s\n",
usd.Format(pr.Credit), usd.Format(pr.Charge), usd.Format(pr.Net))
}
Output: credit -5.00, charge 10.00, net 5.00
Example (Reconciliation) ¶
Example_reconciliation shows that tier lines are allocated from the once-rounded total, so they reconcile exactly even when each tier ends in half a minor unit.
package main
import (
"fmt"
"math/big"
"github.com/zkrebbekx/tariff"
)
func main() {
c := tariff.Charge{
Model: tariff.Graduated,
Currency: tariff.USD(tariff.RoundHalfUp),
Tiers: []tariff.Tier{
{UpTo: 1, UnitRate: big.NewRat(21, 200)}, // $0.105
{UpTo: 2, UnitRate: big.NewRat(41, 200)}, // $0.205
{Last: true, UnitRate: big.NewRat(61, 200)}, // $0.305
},
}
res, _ := c.Rate(3)
var sum int64
for _, l := range res.Lines {
sum += l.Subtotal
}
fmt.Printf("total=%d lines=%d+%d+%d sum=%d\n",
res.Total, res.Lines[0].Subtotal, res.Lines[1].Subtotal, res.Lines[2].Subtotal, sum)
}
Output: total=62 lines=10+21+31 sum=62
Example (Volume) ¶
Example_volume charges the whole quantity at the single rate of the tier it lands in, which is why six units cost 6 x $6.50 rather than the graduated mix.
package main
import (
"fmt"
"math/big"
"github.com/zkrebbekx/tariff"
)
func main() {
c := tariff.Charge{
Model: tariff.Volume,
Currency: tariff.USD(tariff.RoundHalfUp),
Tiers: []tariff.Tier{
{UpTo: 5, UnitRate: big.NewRat(7, 1)},
{UpTo: 10, UnitRate: big.NewRat(13, 2)},
{Last: true, UnitRate: big.NewRat(6, 1)},
},
}
res, _ := c.Rate(6)
fmt.Println(c.Currency.Format(res.Total))
}
Output: 39.00
Index ¶
- Variables
- func Allocate(total int64, ratios []int64) ([]int64, error)
- func NextBoundary(anchor, from time.Time, unit CycleUnit) time.Time
- func NextCalendarBoundary(from time.Time, unit CycleUnit) time.Time
- func Prorate(amount int64, cur Currency, frac *big.Rat) (int64, error)
- type Basis
- type Charge
- type Currency
- type CycleUnit
- type Invoice
- type Line
- type Model
- type Period
- type Proration
- type Result
- type RoundingMode
- type Step
- type Tier
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNegativeQuantity is returned by [Charge.Rate] when the quantity is // negative. A zero quantity is valid and rates to nothing. ErrNegativeQuantity = errors.New("tariff: negative quantity") // ErrNegativeAmount is returned by [Change] when a plan amount is negative. // Proration's Credit-is-negative / Charge-is-positive result only holds for // non-negative plan prices; a negative price is rejected rather than // silently inverting those signs. ErrNegativeAmount = errors.New("tariff: negative amount") // ErrEmptyTiers is returned when a tiered model (graduated, volume, // stairstep) is rated with no tiers at all. ErrEmptyTiers = errors.New("tariff: no tiers") // ErrTierOrder is returned when a tier schedule is malformed: upper bounds // that do not strictly increase, a non-positive bound, a non-final tier // marked unbounded, or a final tier that is not unbounded. Tiers are // half-open on the low side — tier i covers the units in (tiers[i-1].UpTo, // tiers[i].UpTo] — and the final tier must be unbounded so that every // quantity is covered. ErrTierOrder = errors.New("tariff: tiers out of order") // ErrNoRate is returned when a required per-unit or flat rate is missing or // negative: a per-unit charge with a nil rate, a graduated or volume tier // with a nil or negative rate, a stairstep tier with a negative flat rate, // or a negative flat fee on the charge. ErrNoRate = errors.New("tariff: missing or invalid rate") // ErrBadPackage is returned when a package charge is misconfigured: a // package size that is not positive, or a negative package price. ErrBadPackage = errors.New("tariff: invalid package configuration") // ErrBadAllowance is returned when a charge's free allowance is negative. ErrBadAllowance = errors.New("tariff: negative free allowance") // ErrBadCurrency is returned when a currency is unusable: negative or // implausibly large decimal places, or an unset rounding mode. A charge // must always choose its rounding mode explicitly, because a hidden default // is a silent compliance bug. ErrBadCurrency = errors.New("tariff: invalid currency") // ErrBadAllocation is returned by [Allocate] when asked to split across no // parts, or with a negative ratio. The total may be any sign — a negative // total is a proration credit split across lines. ErrBadAllocation = errors.New("tariff: invalid allocation") // ErrOverflow is returned when an exact amount, or an allocated share, does // not fit in an int64 count of minor units. ErrOverflow = errors.New("tariff: amount overflows int64 minor units") // ErrUnknownModel is returned when a charge names a rating model that this // package does not implement. ErrUnknownModel = errors.New("tariff: unknown rating model") // ErrBadPeriod is returned when a billing [Period] is unusable: its End is // not strictly after its Start, or a day-based [Basis] is applied to a // period that spans no whole calendar day (so the day denominator is zero). ErrBadPeriod = errors.New("tariff: invalid billing period") // ErrBadWindow is returned by [Period.Fraction] when the window is // incoherent — from is after to — or by [Prorate] when the fraction is nil. // A window that simply falls outside the period is not an error; it clamps // to a zero fraction. ErrBadWindow = errors.New("tariff: invalid proration window") // ErrBadBasis is returned by [Period.Fraction] when the proration [Basis] // is not one this package implements. ErrBadBasis = errors.New("tariff: unknown proration basis") // ErrBadDiscount is returned by a composition step when a discount is // malformed: a nil percentage, a percentage outside [0, 1], or a negative // fixed amount off. ErrBadDiscount = errors.New("tariff: invalid discount") // ErrBadFloor is returned by [MinimumCharge] when the floor is negative. ErrBadFloor = errors.New("tariff: invalid minimum charge") // ErrBadBalance is returned by [DrawCredit] or [DrawCommitment] when the // balance pointer is nil or the balance it points to is negative. ErrBadBalance = errors.New("tariff: invalid balance") // ErrCurrencyMismatch is returned by [Compose] when a [Charged] step's // currency does not share the invoice currency's code and minor-unit scale, // so its amounts cannot be summed with the rest of the invoice. ErrCurrencyMismatch = errors.New("tariff: currency mismatch") // ErrNilStep is returned by [Compose] when one of the steps is nil. ErrNilStep = errors.New("tariff: nil step") )
Sentinel errors returned (usually wrapped with context) by this package. Match them with errors.Is rather than by comparing error strings.
Functions ¶
func Allocate ¶
Allocate splits a total across len(ratios) parts in proportion to the given ratios, losing nothing. Each part receives the floor (toward zero) of its exact share, then the leftover minor units — always fewer than the number of parts — are handed to the parts whose exact share had the largest fractional remainder (the largest-remainder, or Hamilton, method), ties broken by position. The result sums exactly to total for any ratios and any remainder, is deterministic, and — unlike a round-robin-from-the-first split — never hands a penny to a part that did not round up: a zero ratio receives zero, and a part whose exact share is already whole keeps it.
The total may be negative: a proration credit is a negative amount split across lines. A negative total is allocated as the exact mirror of its magnitude — every share is non-positive, the sum is exactly the (negative) total, and the negative leftover lands on the same largest-remainder parts a positive leftover would. A zero ratio still receives exactly zero. (Phase 1 refused a negative total; that limitation is now lifted.)
This is the penny-safe remainder split that makes line-item reconciliation and proration exact, without misrepresenting what any one line charged. tariff uses the same routine internally to distribute a rounded tiered total back across its tier lines. If every ratio is zero the split is made evenly.
Every ratio must be non-negative; a negative ratio, or no parts at all, returns an error wrapping ErrBadAllocation. The total is unrestricted in sign.
func NextBoundary ¶ added in v0.2.0
NextBoundary returns the first anniversary cycle boundary strictly after from, for a cycle of the given unit anchored at anchor. Boundaries are anchor ± k units for integer k, all in the anchor's location and at the anchor's time of day.
The month-end rule is the delicate part, and it is handled without drift: a boundary's day is the anchor's day of month clamped to the last valid day of the target month, always measured from the original anchor day. An anchor of January 31 therefore steps to February 28 (or 29 in a leap year) and then back to March 31, never sticking at the 28th; a February 29 anchor steps to February 28 in common years and returns to February 29 the next leap year.
func NextCalendarBoundary ¶ added in v0.2.0
NextCalendarBoundary returns the next calendar-aligned cycle boundary strictly after from: the first of the next month for Monthly, or the first January for Yearly, at midnight in from's location. It is the thin wrapper over NextBoundary with the anchor pinned to the first of the period — calendar-aligned cycles are just anniversary cycles anchored on the 1st.
func Prorate ¶ added in v0.2.0
Prorate returns amount scaled by frac, rounded once to the currency's minor unit with the currency's rounding mode. The amount is already an int64 count of minor units; frac is dimensionless, so amount * frac is the prorated amount in minor units, rounded through the same path Charge.Rate uses. A nil frac is ErrBadWindow; an unusable currency is ErrBadCurrency; a result that will not fit an int64 is ErrOverflow.
Types ¶
type Basis ¶ added in v0.2.0
type Basis uint8
Basis selects how Period.Fraction measures elapsed time. The zero value, ProrateBySecond, is the Stripe default and needs no configuration.
const ( // ProrateBySecond measures real elapsed time to the nanosecond. A day that // is 23 or 25 hours long across a DST transition contributes its true // length, so the fraction is never off by the missing or repeated hour. // This is the Stripe default and the zero value of Basis. ProrateBySecond Basis = iota // ProrateByDay measures whole calendar days in the period's location. Every // calendar day counts as exactly one, regardless of a DST transition within // it, matching the Chargebee day-based option. The day containing a // window's lower bound is floored to that day's start, so used and remaining // day counts partition the period exactly. ProrateByDay )
type Charge ¶
type Charge struct {
// Model selects the rating algebra.
Model Model
// Currency fixes the minor-unit scale and rounding mode.
Currency Currency
// UnitRate is the exact per-unit price for the tier-less PerUnit model.
UnitRate *big.Rat
// Tiers is the price schedule for the Graduated, Volume and Stairstep
// models.
Tiers []Tier
// PackageSize is the block size for the Package model; it must be positive.
PackageSize int64
// PackagePrice is the price per block, in minor units, for the Package
// model.
PackagePrice int64
// FreeAllowance is a quantity subtracted before rating (or before the
// package ceil). It composes with every model.
FreeAllowance int64
// FlatFee is an optional fixed amount, in minor units, added to the rated
// total as its own line — the fixed half of a fixed-plus-usage charge. It
// applies regardless of quantity, including when usage rates to nothing.
FlatFee int64
}
Charge is a price-plan component: one rating model, a currency, and the parameters that model reads. Rate it against a usage quantity with Charge.Rate.
The rate fields are exact rationals (*big.Rat) so that sub-cent prices such as $0.0006 never drift; quantity * rate is evaluated entirely in math/big and rounded to the minor unit exactly once, at the line boundary, using the currency's explicit rounding mode.
func (Charge) Rate ¶
Rate computes the charge for a usage quantity, returning the rounded total in minor units and the reconciling line items.
A zero quantity rates the usage to nothing (any FlatFee still applies). A negative quantity is an error. The charge's configuration is validated first, so a malformed schedule is reported even at zero quantity.
type Currency ¶
type Currency struct {
// Code is an informational label such as "USD"; it does not affect
// arithmetic.
Code string
// Decimals is the number of minor-unit decimal places: 2 for cents, 0 for
// whole yen, 3 for fils.
Decimals int
// Rounding is the mode used to round an exact amount to a whole minor unit.
// It must be set explicitly.
Rounding RoundingMode
}
Currency describes the money unit a charge is priced in: the number of decimal places its minor unit has, and how exact amounts are rounded to it. The minor-unit scale is entirely currency-driven — 2 for USD, 0 for JPY, 3 for KWD — and never hardcoded to cents anywhere in the package.
A Currency is a small immutable value; construct one directly or use USD, JPY and KWD for the common cases.
func JPY ¶
func JPY(r RoundingMode) Currency
JPY returns the zero-decimal Japanese yen with the given rounding mode.
func KWD ¶
func KWD(r RoundingMode) Currency
KWD returns the three-decimal Kuwaiti dinar with the given rounding mode.
func USD ¶
func USD(r RoundingMode) Currency
USD returns the two-decimal US dollar with the given rounding mode.
type CycleUnit ¶ added in v0.2.0
type CycleUnit uint8
CycleUnit is the step of a billing cycle: Monthly or Yearly. Monthly is the zero value.
type Invoice ¶ added in v0.2.0
type Invoice struct {
// Currency fixes the minor-unit scale and the rounding mode used by the
// discount steps.
Currency Currency
// Lines are the invoice lines in application order: charge lines followed
// or interleaved with labeled adjustment lines.
Lines []Line
// Subtotal is the sum of the charge lines — the gross before adjustments.
Subtotal int64
// Total is the net amount after every step, in minor units. It equals the
// sum of all line subtotals.
Total int64
// contains filtered or unexported fields
}
Invoice is a computed invoice: the currency it is priced in, the ordered adjustment and charge lines, the gross Subtotal from the charges, and the running Total after every step. It is a value, computed by Compose and stored by no one — composition is not persistence.
The line subtotals always sum exactly to Total: every step appends the exact line it applied and moves Total by that same amount.
func Compose ¶ added in v0.2.0
Compose folds the steps left to right over an empty invoice in the given order, returning the resulting Invoice. The order is the caller's and it is visible in the result: composing a percentage discount before a minimum charge floors after discounting, and swapping the two floors before discounting — tariff computes each order faithfully rather than imposing one.
The currency is validated up front; a nil step is ErrNilStep; any step's own validation error (a bad discount, floor, balance, or a charge whose currency does not match the invoice) propagates unwrapped-through as a sentinel matchable with errors.Is.
type Line ¶
type Line struct {
// Quantity is the number of units this line rates, or the number of blocks
// for a package charge. It is zero on a bare flat-fee line.
Quantity int64
// Rate is the exact per-unit rate applied, or nil where the model has no
// per-unit rate (package, stairstep, flat fee). It is a copy the caller may
// keep or mutate freely.
Rate *big.Rat
// Subtotal is this line's amount in minor units. It is negative on a credit
// or discount line in a composed [Invoice].
Subtotal int64
// Label is a human-readable description of an adjustment line — a discount,
// minimum top-up, credit or commitment draw in a composed [Invoice]. It is
// empty on the rating lines produced by [Charge.Rate].
Label string
}
Line is one component of a rated charge: the units (or blocks) it covers, the exact per-unit rate applied where one applies, and the subtotal in minor units. Across a Result the line subtotals always sum exactly to the total.
type Model ¶
type Model uint8
Model selects a charge's rating algebra.
const ( // PerUnit charges quantity * UnitRate, a single flat rate for every unit. PerUnit Model = iota // Graduated charges each tier's units at that tier's rate and sums the // per-tier subtotals — the marginal, cumulative interpretation of a tiered // schedule. Graduated // Volume charges the whole quantity at the single rate of the one tier the // total lands in. The total can fall as usage grows into a cheaper tier. Volume // Package rounds the chargeable quantity up to whole blocks of PackageSize // (after any free allowance) and charges PackagePrice per block. Package // Stairstep charges a flat fee for landing in a tier, regardless of the // exact quantity within it. Stairstep )
type Period ¶ added in v0.2.0
type Period struct {
// Start is the inclusive lower bound of the period.
Start time.Time
// End is the exclusive upper bound of the period; it must be after Start.
End time.Time
}
Period is a billing period, the half-open instant range [Start, End). Its End must be strictly after its Start. A period is anchored in a *time.Location — Start.Location() is the period's location, the frame in which a day-based Basis counts calendar days and in which cycle boundaries are computed. Second-based fractions are location-independent, being real elapsed time.
func (Period) Fraction ¶ added in v0.2.0
Fraction returns the exact fraction of the period covered by the window [from, to), as a *big.Rat that never crosses float64.
The window is clamped to the period before measuring: the covered span is [max(from, Start), min(to, End)). A window wider than the period yields exactly 1; a window that falls entirely outside the period yields exactly 0; an empty window (from == to) yields 0. Only from strictly after to is an error (ErrBadWindow) — it is not a coherent window. The period itself must be valid, or ErrBadPeriod is returned.
Under ProrateBySecond the fraction is real elapsed nanoseconds of the covered span over real elapsed nanoseconds of the whole period, so a DST day contributes its true length. Under ProrateByDay it is the count of whole calendar days in the covered span over the whole period, computed from civil dates in the period's location, so a DST day contributes exactly one and the result is immune to the 23- or 25-hour day. A day-based fraction requires the period to span at least one whole calendar day, or ErrBadPeriod is returned (its day denominator would be zero).
type Proration ¶ added in v0.2.0
type Proration struct {
// Credit is the credit for the unused remainder of the old price. It is
// zero or negative.
Credit int64
// Charge is the charge for the remaining time on the new price. It is zero
// or positive.
Charge int64
// Net is Charge + Credit, the amount actually billed for the change.
Net int64
}
Proration is the outcome of a mid-period plan change: a negative Credit for the unused remainder of the old price, a positive Charge for the remaining time on the new price, and their Net. It is the verified cross-vendor model — Stripe's "−5 USD unused, 10 USD remaining, 5 USD net" and Chargebee's credit/charge/net — not a true-forward.
func Change ¶ added in v0.2.0
func Change(oldAmount, newAmount int64, cur Currency, p Period, at time.Time, b Basis) (Proration, error)
Change computes the credit, charge and net for a plan change at instant at, partway through period p. The remaining fraction is p.Fraction(at, p.End, b); Credit is −oldAmount × remaining, Charge is +newAmount × remaining, each rounded once, and Net is their sum. A trial-to-paid change falls out with oldAmount = 0, giving a zero credit. Errors from the period, window or currency propagate.
Both amounts must be non-negative — a plan price is not a debt — otherwise ErrNegativeAmount is returned. Given that, Credit is always ≤ 0 and Charge always ≥ 0; a downgrade (newAmount < oldAmount) yields a negative Net, a legitimate refund.
type Result ¶
type Result struct {
// Total is the amount owed, in the currency's minor units.
Total int64
// Lines are the itemized components; their subtotals sum to Total exactly.
Lines []Line
}
Result is the outcome of rating a charge: the rounded total in minor units, and the line items that reconcile to it exactly.
type RoundingMode ¶
type RoundingMode uint8
RoundingMode selects how an exact amount is rounded to a whole minor unit.
The zero value is RoundingUnspecified, which is never a valid mode: tariff refuses to rate a charge whose currency has not chosen one, because a hidden default rounding mode is a silent compliance bug. Different jurisdictions and contracts mandate different modes, so the choice is always the caller's.
const ( // RoundingUnspecified is the zero value and is never valid; a currency must // choose a real rounding mode. RoundingUnspecified RoundingMode = iota // RoundHalfUp rounds to the nearest minor unit, breaking ties away from // zero (0.5 becomes 1, -0.5 becomes -1). This is the common "round half up" // of everyday arithmetic and many billing contracts. RoundHalfUp // RoundHalfEven rounds to the nearest minor unit, breaking ties toward the // even neighbour (0.5 and 1.5 both become their even side). Also known as // banker's rounding; it removes the upward bias of RoundHalfUp. RoundHalfEven // RoundFloor rounds toward negative infinity. RoundFloor // RoundCeil rounds toward positive infinity. RoundCeil )
func (RoundingMode) String ¶
func (m RoundingMode) String() string
String renders the rounding mode for diagnostics.
type Step ¶ added in v0.2.0
type Step interface {
// contains filtered or unexported methods
}
Step is one operation in an invoice composition: a charge, a discount, a minimum, or a credit or commitment draw. The set is sealed — only this package implements it — so Compose over the constructors below is the only way to build an Invoice. Each step appends the labeled line(s) it applies and moves the running total by exactly that amount.
func AmountOff ¶ added in v0.2.0
AmountOff removes a fixed amount of minor units from the running total as a discount. The amount must be non-negative; a negative amount is ErrBadDiscount. It may drive the running total below zero — pair it with a MinimumCharge if a floor is wanted.
func Charged ¶ added in v0.2.0
Charged rates the charge for the given quantity and appends its line items to the invoice, adding its total to both the gross subtotal and the running total. The charge's currency must share the invoice currency's code and minor-unit scale (its rounding mode may differ — that is a legitimate per-charge choice); otherwise Compose returns ErrCurrencyMismatch.
func DrawCommitment ¶ added in v0.2.0
DrawCommitment draws a spend-commitment balance down against the running total, with the same capping and mutation semantics as DrawCredit; the two differ only in intent and the audit label. A nil pointer or a negative balance is ErrBadBalance.
func DrawCredit ¶ added in v0.2.0
DrawCredit draws a prepaid credit balance down against the running total, mutating the caller's balance. The draw is capped at both the running total and the balance and is never negative, so the running total cannot go below zero from a credit and the balance cannot be overdrawn. A nil pointer or a negative balance is ErrBadBalance.
func MinimumCharge ¶ added in v0.2.0
MinimumCharge tops the running total up to floor if it is currently below it, as a single positive adjustment line; if the running total is already at or above the floor it does nothing. The floor must be non-negative; a negative floor is ErrBadFloor.
func PercentOff ¶ added in v0.2.0
PercentOff removes pct of the running total as a discount, rounded once with the invoice currency's rounding mode. pct is a fraction, not a whole percent: big.NewRat(1, 10) is 10% off. It must be non-nil and in [0, 1]; a nil, negative or above-one pct is ErrBadDiscount.
type Tier ¶
type Tier struct {
// UpTo is the inclusive upper bound of this band, in units. It is ignored
// when Last is true.
UpTo int64
// Last marks the final, unbounded band. It must be true for the last tier
// and false for every other.
Last bool
// UnitRate is the exact per-unit price within this band, used by the
// graduated and volume models.
UnitRate *big.Rat
// FlatRate is the flat fee, in minor units, for landing in this band, used
// by the stairstep model.
FlatRate int64
}
Tier is one band of a tiered price schedule. Bands are half-open on the low side: tier i covers the units in (tiers[i-1].UpTo, tiers[i].UpTo], with the first tier covering (0, tiers[0].UpTo]. Exactly one tier — the last — is unbounded, marked with Last, so that every quantity is covered.
Which fields matter depends on the charge's model. Graduated and volume read UnitRate; stairstep reads FlatRate. UpTo is ignored on the last (unbounded) tier.