referrals

package
v1.801.459 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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:

  1. Every org has a STABLE referral code (deriveCode: deterministic base32 of a hash of the org id) and a link https://<brand>/?ref=<code>.
  2. 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).
  3. 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

View Source
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

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires the referrals surface onto app per HIP-0106. Complex flavour: it holds a package-global (mounted) so Shutdown can release the store, so it constructs the Service value directly rather than via cloud.Mount.

func Shutdown

func Shutdown() error

Shutdown closes the referrals store. Idempotent.

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) Close

func (s *Store) Close() error

Close closes the underlying database. Idempotent-safe via sql.DB.

func (*Store) EnsureCode

func (s *Store) EnsureCode(ctx context.Context, org string) (string, error)

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) Get

func (s *Store) Get(ctx context.Context, id string) (Referral, error)

Get re-reads a referral by id (post-transition refresh).

func (*Store) LatchQualified added in v1.801.408

func (s *Store) LatchQualified(ctx context.Context, id string, now int64) (bool, error)

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) ListAll

func (s *Store) ListAll(ctx context.Context, limit int) ([]Referral, error)

ListAll returns every referral newest-first (the admin directory), bounded.

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

func (s *Store) ListPending(ctx context.Context, optReferrer string, limit int) ([]Referral, error)

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).

func (*Store) OrgForCode

func (s *Store) OrgForCode(ctx context.Context, code string) (string, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL