Files
member-console/internal/server/member_extras_test.go
T
cgalo5758 9b96e9c9e9 Rework operator IA and unify UI vocabulary
- Restructure operator sidebar into a flat task list with indented
  children; fold plan topology into plan ladders
- Expand member catalog non-plan section to all published non-tier
  products; require recurring Stripe-mapped prices for purchase
- Add operator domains placements and terminal-claims ledger; redirect
  /domains to the FedWiki Sites Domains anchor
- Apply canonical vocabulary and chrome/form conventions; migrate seeded
  FedWiki Sites display name
2026-08-23 17:12:42 -05:00

184 lines
6.4 KiB
Go

package server
import (
"context"
"database/sql"
"log/slog"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/billing"
"github.com/google/uuid"
)
// TestIsRecurringPrice pins the one-time vs recurring distinction the Extras
// bucket's purchase-affordance gate relies on (resolvePurchasableRecurring):
// one-time-purchase delivery is the schema's reserved, unimplemented arm
// (ux-ia-naming design.md D6) — nothing creates a purchase-linked
// pool_provision from a one-time Stripe charge — so a one-time price must
// never be treated as purchasable even when it otherwise passes
// resolvePurchasable. A one-time price stores recurring_interval NULL
// (00001_init.sql: a bare nullable VARCHAR, no "one_time" literal).
func TestIsRecurringPrice(t *testing.T) {
cases := []struct {
name string
price billing.Price
want bool
}{
{"recurring monthly price", billing.Price{RecurringInterval: sql.NullString{String: "month", Valid: true}}, true},
{"recurring yearly price", billing.Price{RecurringInterval: sql.NullString{String: "year", Valid: true}}, true},
{"one-time price (NULL interval)", billing.Price{RecurringInterval: sql.NullString{}}, false},
{"valid-but-empty interval treated as one-time", billing.Price{RecurringInterval: sql.NullString{String: "", Valid: true}}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := isRecurringPrice(c.price); got != c.want {
t.Errorf("isRecurringPrice(%+v) = %v, want %v", c.price, got, c.want)
}
})
}
}
// TestBuildAddonsDataExtrasBucket covers the member-product-discovery delta's
// Extras bucket against a real DB: published, public, non-tier products with
// display_category values of "addon", some other label, and blank all
// render; a product that is also a tier on a plan ladder is never duplicated
// into Extras regardless of carrying the "addon" label; and a carried label
// surfaces as DisplayCategory without affecting membership.
func TestBuildAddonsDataExtrasBucket(t *testing.T) {
database := newRollbackTestDB(t)
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
bq := billing.New(tx)
sfx := uuid.New().String()[:8]
h := &MemberProductsHandler{BillingQ: bq, Logger: slog.Default()}
mkProduct := func(name, category string) billing.Product {
t.Helper()
p, err := bq.CreateProduct(ctx, billing.CreateProductParams{
Name: name,
DisplayCategory: sql.NullString{String: category, Valid: category != ""},
IsActive: true,
IsPublic: true,
LifecycleStatus: "published",
})
if err != nil {
t.Fatalf("create product %s: %v", name, err)
}
return p
}
labeled := mkProduct("Labeled Extra "+sfx, "addon")
otherLabel := mkProduct("Other-Label Extra "+sfx, "storage")
blank := mkProduct("Blank-Label Extra "+sfx, "")
// A tier that also carries display_category = 'addon': membership is
// structural (ladder-tier presence), so this must render in its plan
// section only and never duplicate into Extras.
tierWithAddonLabel := mkProduct("Tiered Extra "+sfx, "addon")
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
LadderKey: "extras-test-" + sfx, Name: "Extras Test Ladder", IsActive: true,
})
if err != nil {
t.Fatalf("create ladder: %v", err)
}
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID, ProductID: tierWithAddonLabel.ProductID,
}); err != nil {
t.Fatalf("create tier: %v", err)
}
data := h.buildAddonsData(ctx)
if data.Error != "" {
t.Fatalf("unexpected error: %s", data.Error)
}
byID := make(map[string]AddonViewModel, len(data.Addons))
for _, a := range data.Addons {
byID[a.ProductID] = a
}
for _, want := range []billing.Product{labeled, otherLabel, blank} {
if _, ok := byID[want.ProductID]; !ok {
t.Errorf("expected %s to render in Extras", want.Name)
}
}
if _, ok := byID[tierWithAddonLabel.ProductID]; ok {
t.Error("tier product must not be duplicated into Extras regardless of its display_category label")
}
if got := byID[labeled.ProductID].DisplayCategory; got != "addon" {
t.Errorf("labeled product DisplayCategory = %q, want %q", got, "addon")
}
if got := byID[otherLabel.ProductID].DisplayCategory; got != "storage" {
t.Errorf("other-label product DisplayCategory = %q, want %q", got, "storage")
}
if got := byID[blank.ProductID].DisplayCategory; got != "" {
t.Errorf("blank-label product DisplayCategory = %q, want empty", got)
}
// None of these carry a price at all: Purchasable must be false, but
// they still render (purchasability gates the control, not visibility).
for _, id := range []string{labeled.ProductID, otherLabel.ProductID, blank.ProductID} {
if byID[id].Purchasable {
t.Errorf("product %s has no price and must not be Purchasable", id)
}
}
}
// TestBuildAddonsDataOneTimePriceNotPurchasable covers the "purchasability
// gates the control, not the visibility" scenario for a real one-time price:
// a published, public, non-tier product with an active price whose
// recurring_interval is NULL still renders in Extras, but never as
// purchasable.
func TestBuildAddonsDataOneTimePriceNotPurchasable(t *testing.T) {
database := newRollbackTestDB(t)
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
bq := billing.New(tx)
sfx := uuid.New().String()[:8]
product, err := bq.CreateProduct(ctx, billing.CreateProductParams{
Name: "One-Time Extra " + sfx, IsActive: true, IsPublic: true, LifecycleStatus: "published",
})
if err != nil {
t.Fatalf("create product: %v", err)
}
if _, err := bq.CreatePrice(ctx, billing.CreatePriceParams{
ProductID: product.ProductID, Currency: "usd", UnitAmount: 500,
// RecurringInterval intentionally left unset: a one-time price.
}); err != nil {
t.Fatalf("create price: %v", err)
}
h := &MemberProductsHandler{BillingQ: bq, Logger: slog.Default()}
data := h.buildAddonsData(ctx)
if data.Error != "" {
t.Fatalf("unexpected error: %s", data.Error)
}
var found *AddonViewModel
for i := range data.Addons {
if data.Addons[i].ProductID == product.ProductID {
found = &data.Addons[i]
}
}
if found == nil {
t.Fatal("one-time-priced product must still render in Extras")
}
if found.Purchasable {
t.Error("one-time-priced product must not be Purchasable, even with an active price")
}
}