Documentation
¶
Overview ¶
Package referrals is referral ATTRIBUTION: who referred whom, and whether that referee ever became a real customer.
Every org has a stable code and share link, a new org claims it at signup, and the edge advances signup → qualified once the referee actually makes metered spend. That attribution record IS this package's product.
IT MOVES NO MONEY. There is no bonus, no grant, no deposit and no ledger write here. Credit enters an org by exactly two doors — a manual per-org admin grant against the auditable ledger, and an invite that carries credit to a new org — and this package is neither. A referral REWARD is an affiliate PAYABLE: it is tracked in hanzoai/commerce and settled by wire or to a connected wallet, never minted as platform credit.
The loop, end to end:
- Every org has a STABLE referral code (deriveCode: deterministic base32 of a hash of the org id) and a link https://<brand>/?ref=<code>.
- A new org signs up via a link → the console posts POST /v1/referrals/claim with the code → we record referrer↔referee at status signup. Self-referral is blocked; one referral per referee ever (idempotent).
- The referee QUALIFIES once they have made metered spend — the honest signal that they used the product rather than merely signed up. Qualification is a WRITE, so it happens on the admin sweep (POST /v1/admin/referrals/sweep, the cron path) and NOWHERE else. GET /v1/referrals is a pure read: it reports the attribution, it never advances it.
Surface:
GET /v1/referrals (org) my code, link, my referrals POST /v1/referrals/claim (org=referee) record a referral from a ?ref code GET /v1/admin/referrals/bonuses (SuperAdmin) every referral edge + a summary POST /v1/admin/referrals/sweep (SuperAdmin) qualify-check every pending referral
TWO PACKAGES SHARE ONE ADMIN PREFIX. The cross-tenant referral ANALYTICS board (top referrers, conversion) is GET /v1/admin/referrals, owned by apps/affiliates over the shared attribution spine; this package owns the edge directory one segment deeper at /v1/admin/referrals/bonuses. They do not collide, but /v1/admin/referrals/* has no single owner — the merge that gives it one is the standing decision, and it lands in commerce.
serve.go auto-registers GET /v1/referrals/health.
Index ¶
- Constants
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type Referral
- type Store
- func (s *Store) Claim(ctx context.Context, id, referrerOrg, refereeOrg, code string) (Referral, bool, error)
- func (s *Store) Close() error
- func (s *Store) EnsureCode(ctx context.Context, org string) (string, error)
- func (s *Store) Get(ctx context.Context, id string) (Referral, error)
- func (s *Store) LatchQualified(ctx context.Context, id string, now int64) (bool, error)
- func (s *Store) ListAll(ctx context.Context, limit int) ([]Referral, error)
- func (s *Store) ListByReferrer(ctx context.Context, referrerOrg string, limit int) ([]Referral, error)
- func (s *Store) ListPending(ctx context.Context, optReferrer string, limit int) ([]Referral, error)
- func (s *Store) OrgForCode(ctx context.Context, code string) (string, error)
Constants ¶
const ( StatusSignup = "signup" StatusQualified = "qualified" )
Status values. A referral advances signup → qualified. It never moves backward; qualified is terminal (the referee became a real customer, once).
There is deliberately no third state: the old "credited" existed only to record that this package had minted platform credit, and it does not mint anything.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Referral ¶
type Referral struct {
ID string `json:"id"`
ReferrerOrg string `json:"-"` // hidden: the code owner; never leak the other side's org to a referee
RefereeOrg string `json:"-"` // hidden for the same reason (admin view re-exposes both)
Code string `json:"code"` // the referrer code used at claim
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
QualifiedAt int64 `json:"qualifiedAt"`
}
Referral is one referrer↔referee edge. RefereeOrg is UNIQUE across the table — an org can be referred at most once, ever (first-touch attribution), which is also the idempotency key for POST /v1/referrals/claim.
It holds attribution and nothing else. No amount, no ledger transaction: what a qualified referral is WORTH is an affiliate payable in commerce, and pricing it here would be a second answer to a question that already has one.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the referrals database. ONE SQLite file holds every org's codes + referral edges; there is no per-org WHERE on the codes table (a code→org lookup is a GLOBAL directory by design — the referee presents a code minted by ANY org), while every /v1/referrals read is scoped by referrer_org server-side.
func (*Store) Claim ¶
func (s *Store) Claim(ctx context.Context, id, referrerOrg, refereeOrg, code string) (Referral, bool, error)
Claim records a referrer↔referee edge, idempotently. The referee is the VALIDATED caller (never client-supplied). One-per-referee (UNIQUE) makes a repeat claim — with the same OR a different code — a no-op that returns the FIRST recorded referral (first-touch attribution wins). Self-referral is refused. Returns (referral, created).
func (*Store) EnsureCode ¶
EnsureCode returns the org's stable referral code, minting (and persisting) it on first use. Deterministic derivation + a persisted directory row give BOTH a stable code AND an O(1) reverse lookup (OrgForCode), with collision safety.
func (*Store) LatchQualified ¶ added in v1.801.408
LatchQualified atomically advances a referral to qualified: it sets status + qualified_at ONLY while qualified_at is still 0. RowsAffected==1 means THIS caller won the race and observed the transition; 0 means a concurrent sweep already made it. qualified_at is the idempotency latch, so the edge qualifies at-most-once and the audit trail records one transition, not two.
func (*Store) ListByReferrer ¶
func (s *Store) ListByReferrer(ctx context.Context, referrerOrg string, limit int) ([]Referral, error)
ListByReferrer returns an org's referrals (the people IT referred), newest first, bounded by limit.
func (*Store) ListPending ¶
ListPending returns referrals still awaiting the qualify check (status signup), oldest first, bounded — the admin sweep folds over this set. optReferrer scopes to one referrer ("" = all, the admin sweep).