Files
commerce/api/billing/plans_authority_test.go
zeekayandHanzo Dev 38cc55ce9f Serve no annual price as null, not $0
GET /v1/billing/plans served priceAnnual 0 for advisory, dedicated and
enterprise, where the catalog states null: the wire type was a plain
int64, so an absent annual price became a $0 one, which on a plan that
charges advertises a free year of it.

PlanView.PriceAnnual is now *int64 and the embed keeps the catalog's
null. The row keeps money non-nullable and stores "none" as zero, as it
stores a sales call's price beside ContactSales; annualOf reads a zero
annual price on a plan that charges back as null, so the seeded
authority the endpoint reads answers what the embed does. Charging is
unchanged: a year still bills AnnualTotal and is refused where that is 0.

TestAnnualPriceIsTheCatalogs marshals the embed and the seeded authority
through ReadPlans and requires every plan's priceAnnual to be the
catalog's, null included.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 22:17:19 -07:00

202 lines
6.5 KiB
Go

package billing
import (
"testing"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/models/plan"
"github.com/hanzoai/commerce/util/nscontext"
"github.com/hanzoai/commerce/util/test/ae"
)
// TestSeedPlans_SeedsAndIdempotent: first boot seeds all embed plans; a
// second boot is a no-op (count-gated).
func TestSeedPlans_SeedsAndIdempotent(t *testing.T) {
c := ae.NewContext()
defer c.Close()
created, _, err := SeedPlans(c)
if err != nil {
t.Fatalf("seed: %v", err)
}
if created != len(catalog) {
t.Fatalf("first seed created %d, want %d (17 subscription + 3 dns)", created, len(catalog))
}
created2, _, err := SeedPlans(c)
if err != nil {
t.Fatalf("re-seed: %v", err)
}
if created2 != 0 {
t.Fatalf("re-seed created %d, want 0 (idempotent)", created2)
}
}
// TestSeededPricesEqualEmbed: every seeded row's typed money fields equal the
// embed's, so seeding changes NO charge (the whole point — it only makes prices
// editable).
func TestSeededPricesEqualEmbed(t *testing.T) {
c := ae.NewContext()
defer c.Close()
if _, _, err := SeedPlans(c); err != nil {
t.Fatalf("seed: %v", err)
}
adb := plan.AuthorityDB(c)
for _, embed := range catalog {
p := plan.New(adb)
ok, err := p.Query().Filter("Slug=", embed.Slug).Get()
if err != nil || !ok {
t.Fatalf("seeded plan %q missing: ok=%v err=%v", embed.Slug, ok, err)
}
if int64(p.Price) != embed.Price || int64(p.PriceAnnual) != embed.annual() {
t.Errorf("seeded %q price %d/%d != embed %d/%d (seed must not change charge)", embed.Slug, p.Price, p.PriceAnnual, embed.Price, embed.annual())
}
if p.ContactSales != embed.ContactSales {
t.Errorf("seeded %q contactSales %v != embed %v (null-vs-0 preserved)", embed.Slug, p.ContactSales, embed.ContactSales)
}
}
}
// TestEditPrice_ResolveReflects is THE control proof: an admin edit to the plan
// authority's Price is what resolveSubscriptionPlan (→ the internal-ledger charge)
// returns — this is how admin.hanzo.ai governs subscription pricing.
func TestEditPrice_ResolveReflects(t *testing.T) {
c := ae.NewContext()
defer c.Close()
if _, _, err := SeedPlans(c); err != nil {
t.Fatalf("seed: %v", err)
}
// Admin raises "dev" from $19 to $99 in the authority.
adb := plan.AuthorityDB(c)
p := plan.New(adb)
if ok, _ := p.Query().Filter("Slug=", "dev").Get(); !ok {
t.Fatal("dev not seeded")
}
p.Price = 9900
if err := p.Update(); err != nil {
t.Fatalf("edit: %v", err)
}
// resolveSubscriptionPlan — called with ANY org db — reads the NEW price.
orgDB := datastore.New(nscontext.WithNamespace(c, "acme"))
got, err := resolveSubscriptionPlan(orgDB, "dev")
if err != nil {
t.Fatalf("resolve: %v", err)
}
if got.Price != 9900 {
t.Fatalf("resolveSubscriptionPlan(dev).Price = %d, want 9900 (admin edit MUST reflect in the charge)", got.Price)
}
}
// TestResolveSeededEqualsEmbed: the seed itself changes no charge — resolve
// returns the SAME price before (embed fallback) and after (DB row) seeding.
func TestResolveSeededEqualsEmbed(t *testing.T) {
c := ae.NewContext()
defer c.Close()
orgDB := datastore.New(nscontext.WithNamespace(c, "acme"))
embed := lookupPlan("dev")
if embed == nil {
t.Fatal("embed dev missing")
}
// Before seed: authority empty → resolve falls back to the embed VALUE.
before, err := resolveSubscriptionPlan(orgDB, "dev")
if err != nil {
t.Fatalf("resolve pre-seed: %v", err)
}
if int64(before.Price) != embed.Price {
t.Fatalf("pre-seed resolve price %d != embed %d", before.Price, embed.Price)
}
// After seed: resolve reads the DB row — SAME price (seed == embed).
if _, _, err := SeedPlans(c); err != nil {
t.Fatalf("seed: %v", err)
}
after, err := resolveSubscriptionPlan(orgDB, "dev")
if err != nil {
t.Fatalf("resolve post-seed: %v", err)
}
if int64(after.Price) != embed.Price {
t.Fatalf("post-seed resolve price %d != embed %d (seed must not change charge)", after.Price, embed.Price)
}
}
// TestPlanAuthorityRows_LoudFallbackThenAuthority: the read edge signals fallback
// on an empty authority (ListPlans then serves the embed, never blank), and reads
// the DB — with embed-equal content — once seeded.
func TestPlanAuthorityRows_LoudFallbackThenAuthority(t *testing.T) {
c := ae.NewContext()
defer c.Close()
if _, ok := planAuthorityRows(c); ok {
t.Fatal("empty authority must signal fallback (ok=false) so ListPlans serves the embed")
}
if _, _, err := SeedPlans(c); err != nil {
t.Fatalf("seed: %v", err)
}
rows, ok := planAuthorityRows(c)
if !ok {
t.Fatal("seeded authority must read ok")
}
if len(rows) != len(catalog) {
t.Fatalf("authority rows = %d, want %d", len(rows), len(catalog))
}
bySlug := map[string]staticPlan{}
for _, r := range rows {
bySlug[r.Slug] = r
}
for _, embed := range catalog {
got := bySlug[embed.Slug]
if got.Price != embed.Price || got.Category != embed.Category || !sameCents(got.PriceAnnual, embed.PriceAnnual) {
t.Errorf("projected %q = %d/%s/%s, want %d/%s/%s", embed.Slug, got.Price, centsText(got.PriceAnnual), got.Category, embed.Price, centsText(embed.PriceAnnual), embed.Category)
}
}
}
// TestSyncStripeUntouchedByDBEdit: StaticPlans() — the source SyncStripe reads —
// is the EMBED and is unaffected by a DB authority edit. Confirms SyncStripe is
// untouched (deferred Step 3b): editing a plan changes the charge (resolve) but
// NOT the Stripe-dashboard projection source.
func TestSyncStripeUntouchedByDBEdit(t *testing.T) {
c := ae.NewContext()
defer c.Close()
if _, _, err := SeedPlans(c); err != nil {
t.Fatalf("seed: %v", err)
}
before := staticMonth(StaticPlans(), "dev")
adb := plan.AuthorityDB(c)
p := plan.New(adb)
if ok, _ := p.Query().Filter("Slug=", "dev").Get(); !ok {
t.Fatal("pro missing")
}
p.Price = 12345
if err := p.Update(); err != nil {
t.Fatalf("edit: %v", err)
}
after := staticMonth(StaticPlans(), "dev")
// The property is that the DB edit did not reach the embed — not what the
// embed happens to charge. Pinning the price here made a reprice look like a
// broken authority boundary, which is a different fault entirely.
if before != after {
t.Fatalf("StaticPlans(pro) changed by a DB edit: before=%d after=%d (SyncStripe must read the untouched embed)", before, after)
}
if after == 12345 {
t.Fatalf("StaticPlans(pro) = %d, the value written to the DB — the embed is being read through the authority", after)
}
}
func staticMonth(plans []Plan, slug string) int64 {
for _, p := range plans {
if p.Slug == slug {
return p.PriceMonth
}
}
return -1
}