Documentation
¶
Overview ¶
Package billingaccount is the top-level, tenant-independent money-of-record primitive. A BillingAccount is the unit balance is recorded against; entities (users, orgs, projects) BIND to accounts (Binding) so a debit walks an ordered chain and falls to the next account when one cannot cover it.
The account's stable string Id IS its ledger subject key: the append-only commerce ledger keys every row on transaction.SourceId/DestinationId, and a vaulted card keys on paymentmethod.CustomerId — so an account's money and its instruments are exactly the rows carrying its id. Balance is the derived sum over that ledger (models/transaction/util), never a mutable field here. This entity carries only administrative metadata (who administers it, display name, currency, upstream processor handles); it never stores an amount.
Index ¶
Constants ¶
const ( HolderUser = "user" HolderOrg = "org" HolderProject = "project" )
Holder/owner kinds — the ONE vocabulary shared by a binding's HolderKind and an account's OwnerKind, so the two can never name entity classes differently.
const BindingKind = "billing-binding"
BindingKind is the datastore kind for a holder→account binding.
const Kind = "billing-account"
Kind is the datastore kind for a billing account.
Variables ¶
This section is empty.
Functions ¶
func BindingID ¶
BindingID derives the stable storage id for a (holderKind, holderId, accountId) triple. Same inputs → same id → a re-bind upserts onto the one row via the storage ON CONFLICT(id, kind, namespace), so the backfill is idempotent.
func BindingQuery ¶
BindingQuery returns a datastore query over bindings in db's namespace.
func NewAccountID ¶
func NewAccountID() string
NewAccountID mints an opaque, stable id for an account created without a pre-existing ledger subject to adopt. It is deliberately NOT derived from any tenant slug: an account is tenant-independent, so its identity must not encode where it happens to be bound today.
Types ¶
type BillingAccount ¶
type BillingAccount struct {
mixin.Model[BillingAccount]
DisplayName string `json:"displayName,omitempty"`
// OwnerKind/OwnerId name the entity that administers this account (who may
// bind it, add instruments, read its ledger). It is the account's steward,
// NOT a scoping key — the money is keyed on the account Id.
OwnerKind string `json:"ownerKind"`
OwnerId string `json:"ownerId"`
Currency currency.Type `json:"currency" orm:"default:usd"`
// ProviderType/ProviderRef are the upstream processor's handle for this
// account (e.g. a Square/Stripe customer), when one has been provisioned.
ProviderType string `json:"providerType,omitempty"`
ProviderRef string `json:"providerRef,omitempty"`
}
BillingAccount is one account money is recorded against. Its Id is a free-form stable string that doubles as the ledger subject key. Accounts minted fresh take an opaque NewAccountID ("acct_…"); an account backfilled for an existing tenant instead ADOPTS that tenant's current subject string as its id, so the append-only ledger and vaulted cards carry forward with no migration.
func Get ¶
func Get(db *datastore.Datastore, id string) (*BillingAccount, error)
Get loads the account with the given stable id from db's namespace, or returns datastore.ErrNoSuchEntity when absent. The read is key-exact (kind-qualified) so it round-trips on both the SQLite and Postgres backends.
func New ¶
func New(db *datastore.Datastore) *BillingAccount
New returns an initialized BillingAccount bound to db.
type Binding ¶
type Binding struct {
mixin.Model[Binding]
HolderKind string `json:"holderKind"`
HolderId string `json:"holderId"`
AccountId string `json:"accountId"`
Priority int `json:"priority"`
}
Binding maps a holder (a user, org, or project) to a billing account with a priority. A holder's bindings, ordered by ascending Priority (lower is charged first), are the ordered chain a debit walks — falling to the next account when one cannot cover the debit or its card is declined. A holder may bind many accounts (redundancy, personal overflow); an account may be bound by many holders.
The row id is DETERMINISTIC in (HolderKind, HolderId, AccountId), so re-binding the same pair upserts (e.g. to change Priority) rather than forking a duplicate — the property the idempotent backfill relies on.
func Bind ¶
func Bind(db *datastore.Datastore, holderKind, holderId, accountId string, priority int) (*Binding, error)
Bind idempotently records that holder (holderKind, holderId) draws on accountId at the given priority, in db's namespace. Re-binding the same pair updates the priority in place (deterministic id + upsert). It returns the persisted Binding.
func ForHolder ¶
ForHolder returns holder (holderKind, holderId)'s bindings in db's namespace, ascending by Priority (the charge order). Ties break on AccountId so the order is TOTAL and deterministic. An empty holderId yields no rows. Ordering is done in Go, not via a datastore Order clause, so the money-critical charge order is identical on every backend (SQLite/Postgres/in-memory).
func NewBinding ¶
NewBinding returns an initialized Binding bound to db.