Documentation
¶
Index ¶
Constants ¶
const Namespace = "system"
Namespace is the platform-global namespace the plan authority lives in — the SAME "system" namespace the product catalog uses. Subscription and DNS plan pricing is platform-wide and governed centrally by admin.hanzo.ai, NOT per tenant, so every authoritative touch of a plan (the boot seed, the SuperAdmin CRUD, GET /v1/billing/plans, and resolveSubscriptionPlan → the internal-ledger charge) resolves HERE. Kept identical to catalog's "system" so one platform namespace holds all cross-tenant CMS/pricing data.
Variables ¶
var IgnoreFieldMismatch = datastore.IgnoreFieldMismatch
Functions ¶
func AuthorityDB ¶ added in v1.49.12
AuthorityDB returns a datastore scoped (via context — the only namespacing the SQL layer honors) to the platform plan-authority namespace. Pass the request context so tracing/deadlines flow through; a nil context degrades to Background (bootstrap/CLI callers).
func Seed ¶ added in v1.49.12
Seed upserts the given plan rows into db (which MUST be namespaced to the plan-authority Namespace). IDEMPOTENT + NON-DESTRUCTIVE: a plan whose slug already exists is left UNTOUCHED, so an admin edit is never clobbered; only missing slugs are created. The source is injected (the embed lives in api/billing) so the write mechanism stays decoupled from the catalog source. Returns the number created.
func SeedIfEmpty ¶ added in v1.49.12
SeedIfEmpty seeds rows only when NONE of the categories they cover are present yet — a cheap per-category count gates the full per-row create, so it is safe to call on every bootstrap (mirrors catalogentry.SeedInfraIfEmpty). Once any plan in a seeded category exists (seeded, admin-edited, or admin-deleted) the gate stays shut, so admin state — including deletions of individual plans — stays authoritative. A total wipe of every seeded category re-opens the gate; Seed then only re-creates the missing slugs (still non-destructive).
Types ¶
type Plan ¶
type Plan struct {
mixin.Model[Plan]
// Unique human readable id
Slug string `json:"slug"`
// Internal id
SKU string `json:"sku"`
// Human readable name
Name string `json:"name"`
Description string `json:"description"`
// Category is the plan family ("personal"/"team"/"enterprise"/"world"/
// "social"/"dns"); GET /v1/billing/plans?category= filters on it.
Category string `json:"category"`
Price currency.Cents `json:"price"`
// PriceAnnual is the per-month price when billed annually, in cents — the
// authoritative annual price (previously derived at the read edge from the
// embed). Like Price it is a TYPED money field, never an untyped Metadata
// value, so a stored/spoofed plan copy can never inflate it.
PriceAnnual currency.Cents `json:"priceAnnual"`
Currency currency.Type `json:"currency"`
Interval Interval `json:"interval"`
IntervalCount int `json:"intervalCount"`
TrialPeriodDays int `json:"trialPeriodDays"`
// PerSeat marks a plan billed per seat (catalog price_ref.recurring.per_seat):
// invoices charge Price × subscription quantity, floored at 1.
PerSeat bool `json:"perSeat"`
// ContactSales marks a custom / "contact sales" plan whose price is NULL, not
// $0 — the ONE way to preserve the free($0)-vs-custom(null) distinction while
// keeping Price a typed, non-nullable Cents (mirrors the embed's staticPlan).
// A null-priced plan is stored Price=0 + ContactSales=true; a free plan is
// Price=0 + ContactSales=false. Never coerce null→0 WITHOUT this flag, or a
// custom plan spuriously reads as a $0 charge.
ContactSales bool `json:"contactSales,omitempty"`
// Popular flags the highlighted tier within a category (display only).
Popular bool `json:"popular,omitempty"`
Metadata Map `json:"metadata" datastore:"-"`
Metadata_ string `json:"-" datastore:"-"`
Ref refs.EcommerceRef `json:"ref,omitempty"`
}