Files
member-console/internal/server/member_products_test.go
T
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00

162 lines
4.6 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package server_test
import (
"context"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/billing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
"github.com/google/uuid"
)
// TestMemberTierLabel verifies that a member with an active ladder attachment
// sees their tier name, and a member without one sees an off-plan state.
func TestMemberTierLabel(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginMaterializing(ctx, database, nil)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
bq := billing.New(tx)
eq := entitlements.New(tx)
iq := identity.New(tx)
oq := organization.New(tx)
// Set up a plan product on a ladder
es, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
Name: "tier-test-set-" + uuid.New().String()[:8],
})
if err != nil {
t.Fatalf("create entitlement set: %v", err)
}
product, err := bq.CreateProduct(ctx, billing.CreateProductParams{
Name: "Standard Plan",
IsActive: true,
IsPublic: true,
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(es.SetID), Valid: true},
LifecycleStatus: "published",
})
if err != nil {
t.Fatalf("create product: %v", err)
}
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
Name: "Tier Test " + uuid.New().String()[:8],
IsActive: true,
})
if err != nil {
t.Fatalf("create ladder: %v", err)
}
_, err = bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: product.ProductID,
})
if err != nil {
t.Fatalf("create tier: %v", err)
}
// Create org type with default plan, org, person, pool
orgType := "tt-" + uuid.New().String()[:4]
_, err = tx.ExecContext(ctx,
`INSERT INTO core.org_types (org_type, display_name, is_active, default_plan_ladder_id)
VALUES ($1, $2, true, $3)`,
orgType, "Test Type", ladder.PlanLadderID,
)
if err != nil {
t.Fatalf("create org type: %v", err)
}
user, err := iq.CreateUser(ctx, "u-"+uuid.New().String())
if err != nil {
t.Fatalf("create user: %v", err)
}
person, err := iq.CreatePerson(ctx, identity.CreatePersonParams{
UserID: user.UserID,
DisplayName: "Test Member",
PrimaryEmail: "tier-" + uuid.New().String()[:8] + "@example.com",
PrimaryEmailVerified: true,
})
if err != nil {
t.Fatalf("create person: %v", err)
}
org, err := oq.CreateOrganization(ctx, organization.CreateOrganizationParams{
Name: "Tier Test Org",
OrgType: orgType,
OwnerPersonID: person.PersonID,
})
if err != nil {
t.Fatalf("create org: %v", err)
}
pool, err := eq.CreateResourcePool(ctx, entitlements.CreateResourcePoolParams{
OrgID: org.OrgID,
Name: "default",
PoolType: "default",
IsAutoManaged: true,
})
if err != nil {
t.Fatalf("create pool: %v", err)
}
// Scenario A: Pool with active attachment
actor := entitlements.Actor{ActorType: "system", Reason: "test"}
res, err := entitlements.ReapplyDefaultsForPool(ctx, tx, pool.PoolID, actor)
if err != nil {
t.Fatalf("reapply defaults: %v", err)
}
if res.Outcome != "created" || res.ProvisionID == "" {
t.Fatalf("expected a created provision, got outcome=%q provision=%q", res.Outcome, res.ProvisionID)
}
attachments, err := eq.GetActiveAttachmentsByPool(ctx, pool.PoolID)
if err != nil {
t.Fatalf("get attachments: %v", err)
}
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if attachments[0].ProductID != product.ProductID {
t.Fatalf("expected product %s, got %s", product.ProductID, attachments[0].ProductID)
}
// Scenario B: Create another org+pool without attachment
org2, err := oq.CreateOrganization(ctx, organization.CreateOrganizationParams{
Name: "Tier Test Org 2",
OrgType: orgType,
OwnerPersonID: person.PersonID,
})
if err != nil {
t.Fatalf("create org 2: %v", err)
}
pool2, err := eq.CreateResourcePool(ctx, entitlements.CreateResourcePoolParams{
OrgID: org2.OrgID,
Name: "default",
PoolType: "default",
IsAutoManaged: true,
})
if err != nil {
t.Fatalf("create pool 2: %v", err)
}
attachments2, err := eq.GetActiveAttachmentsByPool(ctx, pool2.PoolID)
if err != nil {
t.Fatalf("get attachments 2: %v", err)
}
if len(attachments2) != 0 {
t.Fatalf("expected 0 attachments for pool2, got %d", len(attachments2))
}
}