Documentation
¶
Overview ¶
Package trial implements the new-signup on-ramp: a trialing subscription of the $20/mo entry plan funded with a single unified trial credit.
The model (settled 2026-07-02):
- Entry plan is the $20/mo "pro" plan: its allowance is metered down across usage. After the trial the subscriber is on $20/mo, metered, with overage billed.
- A brand-new signup with NO card gets a 7-day trial; adding a card extends it to 30 days (ExtendForCard). A signup that adds a card up front is just the 7-day path immediately extended to 30 — ExtendForCard starts a 30-day trial when none exists yet.
- The trial allowance is ONE unified credit spendable across compute OR AI: it is an ordinary tag-scoped, expiring Deposit — the SAME mechanism as billing/allotment's included credit — so it nets into the single balance the gateway's prepaid gate (available > 0) reads and both compute and AI usage debit. No separate buckets, no second gate.
Decomplected from the plan catalog: this package never reads @hanzo/plans. The catalog owner (api/billing) wires the entry plan's economics once via SetEntryPlanResolver; here we only orchestrate subscription + credit.
Index ¶
Constants ¶
const ( // PlanSlug is the catalog's $20/mo entry plan. PlanSlug = "pro" // CreditTag marks the one unified trial-credit deposit. Exactly one per // subject; it is both the funding record and the idempotency anchor. CreditTag = "trial-credit" // Kind is the destination kind for IAM-bound balance transactions — the // same kind the balance read/gate resolves (models/transaction/util). Kind = "iam-user" // NoCardTrialDays / CardTrialDays are the trial windows. A card on file // extends the trial from 7 to 30 days. NoCardTrialDays = 7 CardTrialDays = 30 // ProviderType marks trial subscriptions (vs "manual_gift" / "stripe"). ProviderType = "trial" )
Variables ¶
var ErrInvalidSubject = errors.New("trial: invalid subject")
ErrInvalidSubject is returned when the billing subject is empty.
Functions ¶
func SetEntryPlanResolver ¶
func SetEntryPlanResolver(fn func() Plan)
SetEntryPlanResolver wires the catalog-backed resolver for the entry ($20) plan. Called once at startup by the catalog owner (api/billing). Kept as a resolver (not a value) so this package never imports the catalog and startup init-order does not matter. When unset, trial operations are safe no-ops so binaries that never load the catalog still build and run.
Types ¶
type Plan ¶
type Plan struct {
Slug string
Name string
Description string
PriceCents int64 // recurring monthly price after the trial
CreditCents int64 // unified trial credit funded for the trial window
Currency string
}
Plan is the entry-plan projection the trial needs: the recurring price to bill after the trial, and the unified credit to fund during it. The catalog owner supplies it (see SetEntryPlanResolver).
type Result ¶
type Result struct {
Started bool `json:"started"` // this call created a new trial
Extended bool `json:"extended"` // this call extended 7 -> 30 days
Reason string `json:"reason,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"`
Plan string `json:"plan"`
TrialDays int `json:"trialDays"`
TrialEnd time.Time `json:"trialEnd,omitempty"`
CreditCents int64 `json:"creditCents"`
TransactionID string `json:"transactionId,omitempty"`
}
Result describes the outcome of a trial operation.
func ExtendForCard ¶
ExtendForCard extends a subject's trial to 30 days when a card is added.
- Trialing entry-plan subscription found and shorter than 30 days: extend the trial window (from its original start) and stretch the trial credit's expiry to match. (Extended=true)
- No trial yet: start a fresh 30-day trial (the "signup WITH a card" path), which is a no-op for existing users via Start's new-signup gate.
- Trial already >= 30 days, or subject has no eligible trial: no-op.
func Start ¶
Start begins a trial for a brand-new signup: a trialing subscription of the entry plan plus the single unified trial credit. cardPresent selects the window (7 days without a card, 30 with one). isTest routes the credit to the test ledger (org.TestMode()).
New-signup only: if the subject already has a subscription OR any prior balance deposit (an existing/ comped user, or a repeat call), Start is a no-op (Started=false, Reason="not_new"). This is what leaves existing users — and Dave's real $10k ledger — untouched, and makes Start idempotent.
func StartForStore ¶ added in v1.49.16
func StartForStore(db *datastore.Datastore, subject, storeID string, cardPresent bool, isTest bool) (Result, error)
StartForStore begins the entry trial for one store. A store is the billing unit, so prior activity on another store must neither unlock nor disqualify this one.