referrals

package
v1.801.307 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

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:

  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 signed_up. Self-referral is blocked; one referral per referee ever (idempotent).
  3. 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

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

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"`
	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) 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-grant refresh).

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

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.

func (*Store) SetTxns

func (s *Store) SetTxns(ctx context.Context, id, referrerTxn, refereeTxn string) error

SetTxns records the two ledger transaction ids after the deposits land (best- effort receipt; the latch, not this, is the idempotency authority).

Jump to

Keyboard shortcuts

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