plan

package
v1.49.20 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Index

Constants

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

View Source
var IgnoreFieldMismatch = datastore.IgnoreFieldMismatch

Functions

func AuthorityDB added in v1.49.12

func AuthorityDB(ctx context.Context) *datastore.Datastore

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 Query

func Seed added in v1.49.12

func Seed(db *datastore.Datastore, rows []*Plan) (created, corrected int, err error)

Seed reconciles the plan authority to the given embed rows. It is safe to run on EVERY boot (cheap: one point query per row; writes only when needed) and is the ONE seeder — the count-gated variant is gone, because a count gate makes a fixed re-seed a no-op that keeps serving bad rows.

Per embed row:

  • MISSING → create it, Managed=true.
  • present, UNMANAGED → FORCE-CORRECT its typed money fields + envelope to the embed and mark Managed=true. An unmanaged row was written by a subscription-flow path (bundle expansion / lazy resolve) and may be partial or wrong (Red: bundle wrote Price=0) — this is the corrective repair.
  • present, MANAGED → LEAVE. Seed/admin already own it, so an admin price edit is preserved (the whole point of the editable authority).

Idempotent: after the first corrective pass every row is Managed, so later runs write nothing. seedMu makes the per-slug check-then-write atomic in-process. Returns (created, corrected).

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"`

	// Managed marks a row OWNED by the seed or the SuperAdmin CRUD (authoritative).
	// The corrective boot seed force-corrects UNMANAGED rows — accidental partials
	// written by a subscription-flow path (Red: the bundle expansion wrote a
	// Price=0, envelope-less row that under-charged a direct sub) — to the embed,
	// and LEAVES managed rows so an admin price edit is preserved. One flag
	// distinguishes "authoritative" from "accidental", closing the divergence.
	Managed bool `json:"managed,omitempty"`

	// Metadata is the display envelope (features/limits/bundles/includedIn) — the
	// typed money fields above are authoritative, this is presentation. Metadata_
	// MUST be datastore:",noindex" (persisted), NOT "-" (skipped): with "-" the
	// whole envelope Save()s but never round-trips, so a DB read drops
	// limits/minSeats/features (prod: team.minSeats served null). Mirrors
	// catalogentry exactly — the persisted-blob pattern is the one way.
	Metadata  Map    `json:"metadata,omitempty" datastore:"-"`
	Metadata_ string `json:"-" datastore:",noindex"`

	Ref refs.EcommerceRef `json:"ref,omitempty"`
}

func Fake

func Fake(db *datastore.Datastore) *Plan

func New

func New(db *datastore.Datastore) *Plan

func (*Plan) Load

func (p *Plan) Load(ps []datastore.Property) (err error)

func (*Plan) Save

func (p *Plan) Save() (ps []datastore.Property, err error)

func (*Plan) Validator

func (p *Plan) Validator() *val.Validator

Jump to

Keyboard shortcuts

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