Documentation
¶
Overview ¶
Package referrals mounts the Hanzo Cloud /v1/referrals/* viral-loop surface: a native-Go, per-org referral program on Base/SQLite that grants promo cloud credit through the SAME commerce ledger path as clients/admin.grantCredit (the trial/Credit bucket, tag grant:referral). It mirrors clients/crm's structure exactly — one SQLite store, server-side tenant isolation, one Mount, HIP-0106.
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 signed_up. Self-referral is blocked; one referral per referee ever (idempotent).
- When the referee QUALIFIES (the honest signal: they've made metered spend — actually USED the product, not just claimed a welcome grant) we grant BOTH sides trial credit: referrer +$10, referee +$5. The grant is LATCHED at-most-once (credited_at) so no sweep and no concurrent read can double-pay. The qualify check runs lazily when the referrer loads GET /v1/referrals AND via the admin sweep (POST /v1/admin/referrals/sweep, the cron path).
Surface:
GET /v1/referrals (org) my code, link, referrals, credits earned POST /v1/referrals/claim (org=referee) record a referral from a ?ref code GET /v1/admin/referrals/bonuses (SuperAdmin) every one-time bonus referral + a summary POST /v1/admin/referrals/sweep (SuperAdmin) qualify-check every pending referral
The cross-tenant referral ANALYTICS board (top referrers, conversion, multi-level accrual liability) is GET /v1/admin/referrals, owned by clients/affiliates over the shared attribution spine; this package owns the one-time-bonus ledger at /v1/admin/referrals/bonuses so the two admin surfaces compose without colliding.
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) LatchCredit(ctx context.Context, id string, referrerCents, refereeCents, 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)
- func (s *Store) SetTxns(ctx context.Context, id, referrerTxn, refereeTxn string) error
Constants ¶
const ( StatusSignedUp = "signed_up" StatusQualified = "qualified" StatusCredited = "credited" )
Status values. A referral advances signed_up → qualified → credited. It never moves backward; credited is terminal (the bonus was granted, once).
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"`
ReferrerGrantCents int64 `json:"referrerGrantCents"`
RefereeGrantCents int64 `json:"refereeGrantCents"`
ReferrerTxn string `json:"-"`
RefereeTxn string `json:"-"`
CreatedAt int64 `json:"createdAt"`
QualifiedAt int64 `json:"qualifiedAt"`
CreditedAt int64 `json:"creditedAt"`
}
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.
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) LatchCredit ¶
func (s *Store) LatchCredit(ctx context.Context, id string, referrerCents, refereeCents, now int64) (bool, error)
LatchCredit atomically CLAIMS the one-and-only grant for a referral: it sets credited_at (+ status=credited, grant amounts, qualified_at if unset) ONLY when credited_at is still 0. RowsAffected==1 means THIS caller won the race and must perform the deposits; 0 means another sweep already granted (never double-pay). credited_at is the idempotency latch — the money is guaranteed at-most-once.
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 signed_up), oldest first, bounded — the sweep + the lazy-on-read check fold over this set. optReferrer scopes to one referrer ("" = all, the admin sweep).
func (*Store) OrgForCode ¶
OrgForCode reverse-resolves a referral code to its owning org. Trims + upper- cases the client-supplied code so a link pasted in any case still resolves.