Documentation
¶
Overview ¶
Package affiliates mounts the Hanzo Cloud /v1/affiliates/* partner-commission surface: a native-Go, per-org affiliate program on Base/SQLite that pays partners an ONGOING COMMISSION on the metered spend of the customers they refer. It sits next to clients/referrals (a one-time credit for both sides) as the OTHER growth loop — the recurring, partner-revenue one — and mirrors its structure exactly: one SQLite store, server-side tenant isolation, one Mount, HIP-0106, and the SAME commerce ledger path (a credits payout is a grant, tag grant:affiliate).
The loop, end to end:
- An org APPLIES to be an affiliate (POST /v1/affiliates/apply), optionally requesting a vanity code. Staff APPROVE it (POST /v1/admin/affiliates/:id/ approve), which mints the code (vanity if free, else a derived slug) and sets a commission rate (default 20%). The affiliate now has a link https://<brand>/?aff=<code>.
- A new org signs up via the link → the console posts POST /v1/affiliates/ attribute with the code → we record referred_org↔affiliate (first-touch, one per referred org, self-attribution blocked).
- The ACCRUAL SWEEP (POST /v1/admin/affiliates/sweep, the cron path; also lazy on the affiliate's own dashboard read) folds over each affiliate's referred orgs: commission = the referred org's metered spend THIS PERIOD × the rate, accrued into the affiliate's balance as an affiliate_event. The accrual is LATCHED at-most-once per (affiliate, referred_org, period) — a re-run in the same period never double-accrues, mirroring the referral credit latch.
- Staff PAY OUT accrued commission (POST /v1/admin/affiliates/:id/payout): a "credits" method issues a commerce grant into the affiliate's wallet; cash methods (wire/paypal/…) are record-only. A payout can never exceed pending (accrued − paid), guarded atomically.
Surface:
GET /v1/affiliates (org) my status, code, link, referred count, accrued/pending/paid, payouts POST /v1/affiliates/apply (org) apply to the program (optional vanity code) POST /v1/affiliates/attribute (org=referred) record attribution from an ?aff code GET /v1/admin/affiliates (SuperAdmin) every affiliate + a summary POST /v1/admin/affiliates/:id/approve (SuperAdmin) approve + mint the code POST /v1/admin/affiliates/:id/suspend (SuperAdmin) suspend POST /v1/admin/affiliates/:id/payout (SuperAdmin) record a payout (credits → grant; cash → record-only) POST /v1/admin/affiliates/sweep (SuperAdmin) accrue commission for every referred org this period
serve.go auto-registers GET /v1/affiliates/health.
Index ¶
- Constants
- func Mount(app *zip.App, deps cloud.Deps) error
- func Shutdown() error
- type Accrual
- type Affiliate
- type AffiliateReferral
- type Payout
- type Store
- func (s *Store) AffiliateForCode(ctx context.Context, code string) (Affiliate, error)
- func (s *Store) Apply(ctx context.Context, id, org, requestedCode string, rateBps int64) (Affiliate, bool, error)
- func (s *Store) Approve(ctx context.Context, id, wantCode string, now int64) (Affiliate, error)
- func (s *Store) Attribute(ctx context.Context, id, affiliateID, referredOrg, affiliateOrg, code string) (AffiliateReferral, bool, error)
- func (s *Store) Close() error
- func (s *Store) CountReferrals(ctx context.Context, affiliateID string) (int, error)
- func (s *Store) GetByID(ctx context.Context, id string) (Affiliate, error)
- func (s *Store) GetByOrg(ctx context.Context, org string) (Affiliate, error)
- func (s *Store) LatchAccrual(ctx context.Context, accrualID, affiliateID, referredOrg, period string, ...) (bool, error)
- func (s *Store) ListAll(ctx context.Context, limit int) ([]Affiliate, error)
- func (s *Store) ListApproved(ctx context.Context, limit int) ([]Affiliate, error)
- func (s *Store) ListPayouts(ctx context.Context, affiliateID string, limit int) ([]Payout, error)
- func (s *Store) ListReferrals(ctx context.Context, affiliateID string, limit int) ([]AffiliateReferral, error)
- func (s *Store) RecordPayout(ctx context.Context, payoutID, affiliateID string, amountCents int64, ...) (Payout, error)
- func (s *Store) ReferralCountsByAffiliate(ctx context.Context) (map[string]int, error)
- func (s *Store) SetPayoutTxn(ctx context.Context, payoutID, txn string) error
- func (s *Store) Suspend(ctx context.Context, id string, now int64) (Affiliate, error)
- func (s *Store) VoidPayout(ctx context.Context, payoutID, affiliateID string, amountCents int64) error
Constants ¶
const ( StatusApplied = "applied" StatusApproved = "approved" StatusSuspended = "suspended" )
Status values. An affiliate advances applied → approved (and can be suspended). Only an APPROVED affiliate has a code and accrues commission.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Accrual ¶
type Accrual struct {
ID string `json:"id"`
AffiliateID string `json:"affiliateId"`
ReferredOrg string `json:"referredOrg"`
Period string `json:"period"`
SpendCents int64 `json:"spendCents"`
CommissionCents int64 `json:"commissionCents"`
CreatedAt int64 `json:"createdAt"`
}
Accrual is one per-period commission event (the affiliate_event): the referred org's spend for that period × the affiliate's rate. UNIQUE(affiliate, referred, period) makes the sweep at-most-once per period — the commission latch.
type Affiliate ¶
type Affiliate struct {
ID string `json:"id"`
Org string `json:"-"` // the affiliate's own org; admin view re-exposes it
Code string `json:"code"`
RequestedCode string `json:"-"` // vanity code requested at apply, pending staff approval
Status string `json:"status"`
RateBps int64 `json:"rateBps"`
AccruedCents int64 `json:"accruedCents"` // lifetime commission accrued
PaidCents int64 `json:"paidCents"` // lifetime commission paid out
CreatedAt int64 `json:"createdAt"`
ApprovedAt int64 `json:"approvedAt"`
SuspendedAt int64 `json:"suspendedAt"`
}
Affiliate is one partner org enrolled in the commission program. Org is UNIQUE (one affiliate per org); Code is UNIQUE across affiliates (minted on approval, vanity opt-in). RateBps is the commission rate in basis points (2000 = 20%).
func (Affiliate) PendingCents ¶
PendingCents is the commission earned but not yet paid (never negative).
type AffiliateReferral ¶
type AffiliateReferral struct {
ID string `json:"id"`
AffiliateID string `json:"affiliateId"`
ReferredOrg string `json:"referredOrg"`
Code string `json:"code"`
CreatedAt int64 `json:"createdAt"`
}
AffiliateReferral is one referred_org → affiliate attribution edge. ReferredOrg is UNIQUE across the table (an org is attributed to at most one affiliate, ever — first-touch), which is also the idempotency key for POST /v1/affiliates/attribute.
type Payout ¶
type Payout struct {
ID string `json:"id"`
AffiliateID string `json:"affiliateId"`
AmountCents int64 `json:"amountCents"`
Method string `json:"method"`
Reference string `json:"reference"`
Txn string `json:"txn,omitempty"`
CreatedAt int64 `json:"createdAt"`
}
Payout is one recorded disbursement of accrued commission. A "credits" method issues a commerce grant (Txn set); cash methods (wire/paypal/…) are record-only.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the affiliates database. ONE SQLite file holds every org's affiliate record, attribution edges, accrual events, and payouts. A code→affiliate lookup is a GLOBAL directory by design (a referred org presents a code minted by ANY affiliate); every /v1/affiliates read is scoped by the caller's org server-side.
func (*Store) AffiliateForCode ¶
AffiliateForCode reverse-resolves an affiliate code to its APPROVED owner (an un-approved affiliate has no code). Trims + lower-cases the client-supplied code.
func (*Store) Apply ¶
func (s *Store) Apply(ctx context.Context, id, org, requestedCode string, rateBps int64) (Affiliate, bool, error)
Apply enrolls org as an affiliate at status=applied with the default rate, idempotently. requestedCode is the optional vanity code (validated + minted at approval). A repeat apply returns the EXISTING record (first apply wins). Returns (affiliate, created).
func (*Store) Approve ¶
Approve moves an affiliate to approved and mints its code: wantCode wins, else the requested vanity code, else a deterministic derived slug. A non-empty code is validated + uniqueness-enforced (errCodeTaken on collision with another affiliate). Idempotent for the same resolved code.
func (*Store) Attribute ¶
func (s *Store) Attribute(ctx context.Context, id, affiliateID, referredOrg, affiliateOrg, code string) (AffiliateReferral, bool, error)
Attribute records a referredOrg → affiliate edge, idempotently. The referred org is the VALIDATED caller (never client-supplied). One-per-referred-org (UNIQUE) makes a repeat attribute a no-op returning the FIRST edge (first-touch wins). Self-attribution (an affiliate's own org) is refused. Returns (edge, created).
func (*Store) CountReferrals ¶
CountReferrals returns how many orgs an affiliate has referred.
func (*Store) LatchAccrual ¶
func (s *Store) LatchAccrual(ctx context.Context, accrualID, affiliateID, referredOrg, period string, spendCents, commissionCents, now int64) (bool, error)
LatchAccrual atomically records ONE accrual for (affiliate, referredOrg, period) and adds the commission to the affiliate's accrued balance — in a single transaction. RowsAffected on the INSERT is the latch: a UNIQUE violation means this period was already accrued (returns false, no error, no double-accrual). Returns won=true only when THIS call created the accrual and moved the balance.
func (*Store) ListAll ¶
ListAll returns every affiliate newest-first (the admin directory), bounded.
func (*Store) ListApproved ¶
ListApproved returns every approved affiliate (the sweep set), oldest-first.
func (*Store) ListPayouts ¶
ListPayouts returns an affiliate's payout history, newest-first, bounded.
func (*Store) ListReferrals ¶
func (s *Store) ListReferrals(ctx context.Context, affiliateID string, limit int) ([]AffiliateReferral, error)
ListReferrals returns an affiliate's attribution edges (the orgs it referred), newest-first, bounded.
func (*Store) RecordPayout ¶
func (s *Store) RecordPayout(ctx context.Context, payoutID, affiliateID string, amountCents int64, method, reference string, now int64) (Payout, error)
RecordPayout atomically RESERVES amountCents against the affiliate's pending commission (accrued − paid) and records a payout row — in one transaction. The WHERE guard `(accrued_cents − paid_cents) >= amount` makes it impossible to pay out more than is owed, even under concurrency (RowsAffected 0 → errInsufficient Pending). The commerce grant (for a credits payout) happens AFTER, outside the tx; SetPayoutTxn records the receipt. errNotFound if the affiliate is missing.
func (*Store) ReferralCountsByAffiliate ¶
ReferralCountsByAffiliate returns affiliate_id → referred-org count in ONE GROUP BY (the admin directory's per-row count, no N+1 fan-out).
func (*Store) SetPayoutTxn ¶
SetPayoutTxn records the commerce ledger transaction id after a credits payout deposit lands (best-effort receipt; the pending reservation is the authority).
func (*Store) Suspend ¶
Suspend moves an affiliate to suspended (its code stops resolving for new attribution; earned commission is unaffected). errNotFound if missing.
func (*Store) VoidPayout ¶
func (s *Store) VoidPayout(ctx context.Context, payoutID, affiliateID string, amountCents int64) error
VoidPayout reverses a RecordPayout that could not be BACKED by the treasury reserve: it deletes the payout row and restores the reserved amount to pending (paid_cents −= amount), in one transaction. It is the compensating action when the fund cannot cover a payout the pending-guard already reserved — so a blocked payout leaves the affiliate's pending intact, honestly, instead of silently burning it.