Documentation
¶
Overview ¶
Package invite is the commerce paywall's invite-code engine: a platform-minted code that grants ONE org subscription-free access to the commerce admin.
It reimplements the cloud referrals code→org primitive (OrgForCode / Claim) natively on commerce's datastore — commerce never imports cloud. The design is the same one: a GLOBAL directory (the "system" namespace, like Organization / the platform catalog) so a code minted by the platform is redeemable by ANY org, keyed on the code with first-touch, idempotent binding.
One Invite row per code. Its storage id is DETERMINISTIC on the normalized code (DeterministicID), so:
- Mint is idempotent: re-minting the same code resolves to the SAME row via the storage ON CONFLICT(id,kind,namespace) upsert — never a duplicate.
- Redeem is first-touch: the first org to redeem a code claims it; a repeat redeem by the SAME org is a no-op replay, and a redeem of an already-claimed code by a DIFFERENT org is refused.
Index ¶
Constants ¶
const Namespace = "system"
Namespace is the global directory the invite records live in — the same "system" namespace Organization / the platform catalog use, so a platform-minted code is visible to (and redeemable by) every tenant. Never a per-org namespace: the redeeming org is not known at mint time.
Variables ¶
var ( // ErrUnknownCode — no invite exists for the presented code. ErrUnknownCode = errors.New("invite: unknown code") // ErrAlreadyRedeemed — the code was already claimed by a DIFFERENT org. ErrAlreadyRedeemed = errors.New("invite: code already redeemed by another org") // ErrEmptyCode / ErrEmptyOrg — malformed input. ErrEmptyCode = errors.New("invite: empty code") ErrEmptyOrg = errors.New("invite: empty org") )
Sentinel errors mapped to HTTP status by the handlers.
Functions ¶
func DeterministicID ¶
DeterministicID derives the stable storage id for a normalized code, so mint is idempotent and each code is a single row (ON CONFLICT dedup at storage).
func Normalize ¶
Normalize canonicalizes a code: trimmed + upper-cased so a code pasted in any case resolves. Kept in one place so mint and redeem agree.
func OrgRedeemed ¶
OrgRedeemed reports whether org holds a redeemed invite — the paywall's invite allow-path. db must be the SystemDB. A single-field query (Org=) then an in-memory Redeemed check avoids a two-equality-filter composite index (which is not guaranteed across datastore backends — see the same rule in billing/trial).
Types ¶
type Invite ¶
type Invite struct {
mixin.Model[Invite]
// Code is the normalized (upper-cased, trimmed) invite code — also the
// natural key the deterministic storage id is derived from.
Code string `json:"code"`
// Org is the org that redeemed the code (empty until redeemed). Once set it
// is immutable — first-touch attribution.
Org string `json:"org,omitempty"`
// Note is optional free text captured at mint time (who/why), for audit.
Note string `json:"note,omitempty"`
// Redeemed latches true on the first successful redeem.
Redeemed bool `json:"redeemed"`
CreatedAt time.Time `json:"createdAt"`
RedeemedAt time.Time `json:"redeemedAt,omitempty"`
}
Invite is one code→org grant. Redeemed+Org record the first-touch binding.
func Mint ¶
Mint creates (or returns the existing) invite for a code. Idempotent on the normalized code: minting the same code twice returns the same row rather than a duplicate — the deterministic id makes concurrent first-mints collapse to ONE row via the storage ON CONFLICT(id,kind,namespace) upsert. db must be the SystemDB.
func Redeem ¶
Redeem binds a code to org, first-touch. Returns (invite, redeemedNow, err):
- unknown code → ErrUnknownCode
- already redeemed by the SAME org → (invite, false, nil) — idempotent replay
- already redeemed by a DIFFERENT org → ErrAlreadyRedeemed
- fresh → (invite, true, nil) — org bound
db must be the SystemDB. org is the VALIDATED caller's org (never client-supplied).