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 cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type Accrual
- type Affiliate
- type AffiliateReferral
- type LeaderboardEntry
- type Link
- type OrgEarning
- type Payout
- type PeriodEarning
- type Store
- func (s *Store) AccrualsForSource(ctx context.Context, referredOrg string) ([]Accrual, error)
- func (s *Store) Accrue(ctx context.Context, accrualID, affiliateID, referredOrg, period string, ...) (moved bool, err error)
- func (s *Store) AccruedByLevel(ctx context.Context) (map[int]int64, error)
- func (s *Store) AffiliateForCode(ctx context.Context, code string) (Affiliate, error)
- func (s *Store) AllReferredOrgs(ctx context.Context, limit int) ([]string, error)
- func (s *Store) Apply(ctx context.Context, id, org, ownerUser, 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) ConversionsByCode(ctx context.Context, affiliateID string) (map[string]int, error)
- func (s *Store) CountLinks(ctx context.Context, affiliateID string) (int, error)
- func (s *Store) CountReferrals(ctx context.Context, affiliateID string) (int, error)
- func (s *Store) CreateLink(ctx context.Context, id, affiliateID, code, label string, now int64) (Link, error)
- func (s *Store) DownlineByLevel(ctx context.Context, ancestorOrg string, depth int) (map[string]int, error)
- func (s *Store) EarningsByPeriod(ctx context.Context, affiliateID string, limit int) ([]PeriodEarning, error)
- func (s *Store) EarningsByReferredOrg(ctx context.Context, affiliateID string, limit int) ([]OrgEarning, error)
- func (s *Store) EnsureLink(ctx context.Context, id, affiliateID, code, label string, now int64) error
- func (s *Store) FlushClicks(ctx context.Context, tally map[string]int64) 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) LeaderboardTop(ctx context.Context, limit int) ([]LeaderboardEntry, 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) ListLinks(ctx context.Context, affiliateID string, limit int) ([]Link, 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) RankOf(ctx context.Context, id string, accruedCents int64) (rank, total int, err 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) ReferredOrgCounts(ctx context.Context) (total, converted int, err error)
- func (s *Store) SetHandle(ctx context.Context, id, handle string) (Affiliate, error)
- func (s *Store) SetPayoutTxn(ctx context.Context, payoutID, txn string) error
- func (s *Store) SetRate(ctx context.Context, id string, rateBps int64) (Affiliate, error)
- func (s *Store) SetUserReferrer(ctx context.Context, referredUser, referrerUser, code string) (bool, error)
- func (s *Store) SignupsByCode(ctx context.Context, affiliateID string) (map[string]int, error)
- func (s *Store) Suspend(ctx context.Context, id string, now int64) (Affiliate, error)
- func (s *Store) UplineOrgs(ctx context.Context, sourceOrg string, depth int) ([]string, error)
- func (s *Store) UplineUsers(ctx context.Context, user string, depth int) ([]string, 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"`
Level int `json:"level"`
SpendCents int64 `json:"spendCents"` // the source org's gross charge (revenue), unchanged truth
MarginCents int64 `json:"marginCents"` // Hanzo's margin on that spend — the share base
CommissionCents int64 `json:"commissionCents"`
CreatedAt int64 `json:"createdAt"`
}
Accrual is one per-period PROFIT-SHARE event (the affiliate_event) — the derived share-ledger row keyed by referrer. For the source org's period it records the source's gross metered spend (SpendCents, the customer charge, unchanged truth), the MARGIN Hanzo earned on it (MarginCents = spend × the platform margin fraction), and the affiliate's SHARE of that margin (CommissionCents = margin × the level rate). The share comes OUT OF Hanzo's margin — never the customer's bill — so the invariant CommissionCents ≤ MarginCents holds by construction (level rate ≤ 100%). UNIQUE(affiliate, referred, period) makes the sweep at-most-once per period — the commission latch. Level is the upline distance from the source org to this affiliate (1=direct, 2, 3); a given (affiliate, source, period) has exactly one level because the graph is a forest.
type Affiliate ¶
type Affiliate struct {
ID string `json:"id"`
Org string `json:"-"` // the affiliate's own org; admin view re-exposes it
OwnerUser string `json:"-"` // the user who applied; the head of this affiliate's user-referral chain
Code string `json:"code"`
RequestedCode string `json:"-"` // vanity code requested at apply, pending staff approval
Status string `json:"status"`
RateBps int64 `json:"rateBps"` // the affiliate's DIRECT (L1) commission rate; upline levels use platform constants
AccruedCents int64 `json:"accruedCents"` // lifetime commission accrued
PaidCents int64 `json:"paidCents"` // lifetime commission paid out
// Handle is the OPT-IN public leaderboard display name. Empty ⟹ NOT listed on the
// public leaderboard by name; the affiliate's own rank stays private-visible to
// itself. The org identity is NEVER exposed on the leaderboard — only this
// self-chosen handle, for affiliates who opt in.
Handle string `json:"handle,omitempty"`
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"`
ReferrerOrg string `json:"referrerOrg"`
Code string `json:"code"`
CreatedAt int64 `json:"createdAt"`
}
AffiliateReferral is one referred_org → affiliate attribution edge — ALSO the referredBy graph edge. ReferredOrg is UNIQUE across the table (an org is attributed to at most one affiliate, ever — first-touch, i.e. referredBy is set-once/immutable), which is also the idempotency key for POST /v1/affiliates/ attribute. ReferrerOrg is the attributing affiliate's own org, denormalized so the upline climb is a pure edge walk (referred_org → referrer_org → …) with no per-hop join. The recursive walk over these edges IS the multi-level upline.
type LeaderboardEntry ¶
type LeaderboardEntry struct {
AffiliateID string `json:"-"`
Handle string `json:"handle"`
AccruedCents int64 `json:"accruedCents"`
ReferredCount int `json:"referredCount"`
}
LeaderboardEntry is one ranked affiliate for the privacy-preserving leaderboard: the opt-in handle (never the org), the aggregate accrued share, and the referred count. The affiliate id is carried only so the handler can flag the caller's OWN row; it is never emitted for other affiliates.
type Link ¶
type Link struct {
ID string `json:"id"`
AffiliateID string `json:"-"`
Code string `json:"code"`
Label string `json:"label"`
Clicks int64 `json:"clicks"`
CreatedAt int64 `json:"createdAt"`
}
Link is one shareable referral link an affiliate created: a unique code (in the SAME global directory as an affiliate's primary code, so either resolves an attribution) plus a label and a click counter. The primary code is mirrored as a link row on approval so click tracking is uniform across every code. Signups and conversions are DERIVED (counted from the attribution + accrual tables by code), never stored, so they can never drift from the ledger.
type OrgEarning ¶
type OrgEarning struct {
ReferredOrg string `json:"referredOrg"`
MarginCents int64 `json:"marginCents"`
CommissionCents int64 `json:"commissionCents"`
}
OrgEarning is one row of an affiliate's per-referred-org contribution: AGGREGATE margin + share attributed to that org across all periods. It is the affiliate's OWN downline (orgs it referred) — aggregate only, never the referred org's raw usage.
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 PeriodEarning ¶
type PeriodEarning struct {
Period string `json:"period"`
MarginCents int64 `json:"marginCents"`
CommissionCents int64 `json:"commissionCents"`
}
PeriodEarning is one row of an affiliate's per-period share ledger: the margin base and the share earned in that period, summed over every referred org + level.
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) AccrualsForSource ¶
AccrualsForSource returns every accrual generated by ONE source org's spend, across the upline levels — the per-event share-ledger rows for that source. It is the drill-down that proves the invariant Σ(commission) ≤ margin for a source, and each row's MarginCents is the same source margin base. Ordered by level.
func (*Store) Accrue ¶
func (s *Store) Accrue(ctx context.Context, accrualID, affiliateID, referredOrg, period string, level int, spendCents, marginCents, commissionCents, now int64) (moved bool, err error)
Accrue records — or, for the still-open current period, TOPS UP — the one accrual for (affiliate, referredOrg, period) and moves the affiliate's accrued balance by the change, in a single transaction. UNIQUE(affiliate, referred, period) is the latch key.
Why top-up: the period key is monthly and the source spend is MONTH-TO-DATE (commerce's usage rollup), so one period is swept many times as the month fills in. The FIRST sweep inserts the row; each later sweep in the SAME period recomputes the share from the current (higher) month-to-date spend and SETS the row to it, adding only the positive delta to accrued_cents. The share therefore CONVERGES to the true month-end value instead of freezing at whatever partial spend the first sweep happened to read.
Monotone + bounded by construction:
- month-to-date spend never decreases, so a later commissionCents is never smaller; a lower-or-equal recompute (an unchanged, late, or corrected reading) is a NO-OP, so accrued_cents only ever RISES and never overshoots the month-end value (each reading ≤ the final month-to-date).
- the row always stores margin + commission from the SAME spend reading, so the per-event invariant commissionCents ≤ marginCents holds at EVERY step, and the level schedule cap keeps Σ(commission) over the levels ≤ that source's margin — the money invariant is untouched by the top-up.
Returns moved=true when THIS call changed the balance (an insert or a top-up), so the caller counts real accrual movements and writes one audit row per movement. level is the upline distance recorded for analytics (1=direct, 2, 3), fixed per (affiliate, source) by the forest, so only the spend-derived columns move on a top-up.
func (*Store) AccruedByLevel ¶
AccruedByLevel returns level → total commission accrued at that upline level, the analytics breakdown of platform commission liability by depth.
func (*Store) AffiliateForCode ¶
AffiliateForCode reverse-resolves an affiliate code to its APPROVED owner (an un-approved affiliate has no code). It matches the affiliate's PRIMARY code first, then any secondary shareable link code — both live in the ONE global code directory. Trims + lower-cases the client-supplied code. Only an approved affiliate resolves (a suspended/applied affiliate's codes stop attributing).
func (*Store) AllReferredOrgs ¶
AllReferredOrgs returns every org that has a referredBy edge (the source set the admin sweep folds over), oldest-first, bounded.
func (*Store) Apply ¶
func (s *Store) Apply(ctx context.Context, id, org, ownerUser, 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); ownerUser is the applying user (the head of this affiliate's user chain). 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 (referrer = affiliateOrg), idempotently — this is the set-once referredBy edge of the upline graph. 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, immutable). Self-attribution is refused; an edge that would close an upline loop is refused (errCycle). Returns (edge, created).
func (*Store) ConversionsByCode ¶
ConversionsByCode returns code → count of DISTINCT referred orgs (attributed with that code) that produced positive commission for THIS affiliate. Scoped by affiliate_id (the leading bound predicate).
func (*Store) CountLinks ¶
CountLinks returns how many links an affiliate has (the per-affiliate cap guard).
func (*Store) CountReferrals ¶
CountReferrals returns how many orgs an affiliate has referred.
func (*Store) CreateLink ¶
func (s *Store) CreateLink(ctx context.Context, id, affiliateID, code, label string, now int64) (Link, error)
CreateLink records a new shareable link. The code must be valid + free across the ONE global directory (no affiliate primary code, no other link). errInvalidCode on a malformed code, errCodeTaken on a collision. Returns the created row.
func (*Store) DownlineByLevel ¶
func (s *Store) DownlineByLevel(ctx context.Context, ancestorOrg string, depth int) (map[string]int, error)
DownlineByLevel walks DOWN from ancestorOrg over referredBy edges to `depth`, returning source org → its level below ancestorOrg (1=direct child, 2, 3). This is the per-affiliate accrual set for the lazy dashboard sweep (mirror of UplineOrgs).
func (*Store) EarningsByPeriod ¶
func (s *Store) EarningsByPeriod(ctx context.Context, affiliateID string, limit int) ([]PeriodEarning, error)
EarningsByPeriod returns an affiliate's per-period share ledger (margin base + share earned), newest period first, bounded. Scoped by affiliate_id (leading bound).
func (*Store) EarningsByReferredOrg ¶
func (s *Store) EarningsByReferredOrg(ctx context.Context, affiliateID string, limit int) ([]OrgEarning, error)
EarningsByReferredOrg returns the AGGREGATE margin + share an affiliate earned per org it DIRECTLY referred (level 1), largest share first, bounded. Restricting to the direct level means the breakdown never exposes a sub-downline org's identity to an upline affiliate that did not refer it (deeper-level earnings still count in the period + lifetime totals). Aggregate totals only — never the referred org's raw usage. Scoped by affiliate_id (leading bound predicate).
func (*Store) EnsureLink ¶
func (s *Store) EnsureLink(ctx context.Context, id, affiliateID, code, label string, now int64) error
EnsureLink idempotently mirrors an affiliate's PRIMARY code as a link row (called on approval so click tracking is uniform across every code). A pre-existing code is a silent no-op — never an error.
func (*Store) FlushClicks ¶
FlushClicks folds a batch of coalesced click tallies (code → count) into affiliate_links in ONE transaction (clicks += n per code); an unknown code no-ops. This is the ONLY path that writes the vanity click counter to the money DB, and it is batched + read/shutdown-driven (never per-click), so a public click flood can never contend with the accrual/payout write path. Clicks are never read by any accrual or payout, so a tally lost on a flush error or a crash is harmless.
func (*Store) LeaderboardTop ¶
LeaderboardTop returns the top approved affiliates by lifetime accrued share (descending, id tiebreak) with handle + referred count, bounded. The handler applies the opt-in privacy filter (only handled rows are public) and flags the caller's own row. NEVER exposes org identity.
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) ListLinks ¶
ListLinks returns an affiliate's links, primary first (created_at ASC), bounded.
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) RankOf ¶
func (s *Store) RankOf(ctx context.Context, id string, accruedCents int64) (rank, total int, err error)
RankOf returns the exact 1-based rank of an affiliate among all APPROVED affiliates by accrued share (rank 1 = highest), plus the total approved count — computed over the WHOLE set (not a truncated list), so the caller's own rank is always accurate. Ties break by id (a stable, deterministic order). Returns (0,total,nil) if the affiliate is not approved (no rank).
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) ReferredOrgCounts ¶
ReferredOrgCounts returns the total number of distinct referred orgs and the number that have CONVERTED (produced at least one positive commission accrual) — the conversion numerator/denominator for the admin analytics board.
func (*Store) SetHandle ¶
SetHandle sets an affiliate's opt-in public leaderboard handle (empty clears it, removing the affiliate from the public board by name). Returns the refreshed row.
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) SetRate ¶
SetRate sets an affiliate's DIRECT (L1) commission rate in basis points. The admin handler validates the range (it must leave headroom for the L2/L3 upline so the per-event share can never exceed the margin); the store persists it.
func (*Store) SetUserReferrer ¶
func (s *Store) SetUserReferrer(ctx context.Context, referredUser, referrerUser, code string) (bool, error)
SetUserReferrer records referredUser → referrerUser, set-once (PRIMARY KEY) and cycle-checked (mirrors the org edge). Returns created=false when the user already has a referrer (immutable — first wins) or the pair is a self/cycle no-op signalled by the error. errSelfAttribution when referred==referrer; errCycle on a loop.
func (*Store) SignupsByCode ¶
SignupsByCode returns code → count of orgs THIS affiliate attributed with that code. Scoped by affiliate_id (the leading bound predicate).
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) UplineOrgs ¶
UplineOrgs returns the ancestors of sourceOrg by climbing referredBy edges, up to `depth` levels — index 0 is the direct (L1) referrer, index 1 its referrer (L2), etc. The climb stops at a root, at `depth`, or on a revisited node (forest-safety).
func (*Store) UplineUsers ¶
UplineUsers returns a user's ancestors up to `depth` (index 0 = direct referrer).
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.