Replace product-kind branching and direct position writes with enclosed database functions driven by structural product shape. Migrate grant and provision data, unify operator issuance, update subscription and expiry flows, and add migration and integration proofs.
224 lines
7.1 KiB
Go
224 lines
7.1 KiB
Go
package server_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// TestTrialGrantLifecycle verifies that conferring a trial grant moves the
|
|
// pool to the trial tier, and that ending the conferral (simulating expiry)
|
|
// plus re-applying defaults returns the pool to the default tier.
|
|
func TestTrialGrantLifecycle(t *testing.T) {
|
|
database := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
tx, err := database.BeginTx(ctx, 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)
|
|
|
|
// 1. Set up two-tier ladder with a default product at rank 0
|
|
es0, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
|
|
Name: "trial-test-set-0-" + uuid.New().String()[:8],
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create entitlement set 0: %v", err)
|
|
}
|
|
es1, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
|
|
Name: "trial-test-set-1-" + uuid.New().String()[:8],
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create entitlement set 1: %v", err)
|
|
}
|
|
|
|
defProd, err := bq.CreateProduct(ctx, billing.CreateProductParams{
|
|
Name: "Default Plan",
|
|
IsActive: true,
|
|
IsPublic: true,
|
|
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(es0.SetID), Valid: true},
|
|
LifecycleStatus: "published",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create default product: %v", err)
|
|
}
|
|
|
|
trialProd, err := bq.CreateProduct(ctx, billing.CreateProductParams{
|
|
Name: "Trial Plan",
|
|
IsActive: true,
|
|
IsPublic: true,
|
|
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(es1.SetID), Valid: true},
|
|
LifecycleStatus: "published",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create trial product: %v", err)
|
|
}
|
|
|
|
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
|
|
LadderKey: "trial-ladder-" + uuid.New().String()[:8],
|
|
Name: "Trial Ladder",
|
|
IsActive: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create ladder: %v", err)
|
|
}
|
|
|
|
_, err = bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
|
|
PlanLadderID: ladder.PlanLadderID,
|
|
ProductID: defProd.ProductID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create tier 0: %v", err)
|
|
}
|
|
_, err = bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
|
|
PlanLadderID: ladder.PlanLadderID,
|
|
ProductID: trialProd.ProductID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create tier 1: %v", err)
|
|
}
|
|
|
|
// 2. Create org type with default plan, org, person, pool
|
|
orgType := "tr-" + 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, "Trial 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: "Trial User",
|
|
PrimaryEmail: "trial-" + 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: "Trial Org",
|
|
Slug: "trial-org-" + uuid.New().String()[:8],
|
|
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",
|
|
Slug: "default",
|
|
PoolType: "default",
|
|
IsAutoManaged: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create pool: %v", err)
|
|
}
|
|
|
|
actor := entitlements.Actor{
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(person.PersonID), Valid: true},
|
|
Reason: "test",
|
|
}
|
|
|
|
// 3. Apply default → pool delivers the rank-0 default product
|
|
res0, err := entitlements.ReapplyDefaultsForPool(ctx, tx, pool.PoolID, actor)
|
|
if err != nil {
|
|
t.Fatalf("reapply defaults: %v", err)
|
|
}
|
|
if res0.AlreadyAtTier || res0.Outcome != "created" {
|
|
t.Fatalf("expected first default to create a provision, got %+v", res0)
|
|
}
|
|
|
|
// 4. Confer a trial grant for the rank-1 product — the conferral
|
|
// primitive derives the ladder move from the product's shape, superseding
|
|
// the default position.
|
|
res1, err := entitlements.ConferGrantTx(ctx, tx, entitlements.ConferGrantInput{
|
|
ProductID: trialProd.ProductID,
|
|
OrgID: org.OrgID,
|
|
GrantedByPersonID: person.PersonID,
|
|
GrantReason: "evaluation",
|
|
Description: "trial grant",
|
|
Quantity: 1,
|
|
ValidUntil: sql.NullTime{Time: time.Now().Add(24 * time.Hour), Valid: true},
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(person.PersonID), Valid: true},
|
|
TransitionReason: "test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("confer trial grant: %v", err)
|
|
}
|
|
if res1.Outcome != "created" {
|
|
t.Fatalf("expected trial conferral outcome 'created', got %q", res1.Outcome)
|
|
}
|
|
if res1.PoolID != pool.PoolID {
|
|
t.Fatalf("expected conferral on pool %s, got %s", pool.PoolID, res1.PoolID)
|
|
}
|
|
|
|
attachments, err := eq.GetActiveAttachmentsByPool(ctx, pool.PoolID)
|
|
if err != nil {
|
|
t.Fatalf("get active attachments after trial: %v", err)
|
|
}
|
|
if len(attachments) != 1 || attachments[0].ProductID != trialProd.ProductID {
|
|
t.Fatalf("expected the trial product to be the sole active attachment, got %+v", attachments)
|
|
}
|
|
|
|
// 5. Simulate expiry: end the grant-backed conferral, then re-apply the
|
|
// org default (the expiry path's two steps).
|
|
ended, err := eq.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(res1.Grant.GrantID), Valid: true},
|
|
ActorType: "system",
|
|
Reason: sql.NullString{String: "grant-expiration:" + res1.Grant.GrantID, Valid: true},
|
|
EffectiveAt: time.Now(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("end trial conferral: %v", err)
|
|
}
|
|
if len(ended) != 1 {
|
|
t.Fatalf("expected 1 ended provision, got %d", len(ended))
|
|
}
|
|
|
|
res2, err := entitlements.ReapplyDefaultsForPool(ctx, tx, pool.PoolID,
|
|
entitlements.Actor{ActorType: "system", Reason: "grant-expiration:" + res1.Grant.GrantID})
|
|
if err != nil {
|
|
t.Fatalf("reapply defaults after expiry: %v", err)
|
|
}
|
|
if res2.AlreadyAtTier {
|
|
t.Fatal("expected the default to be re-conferred after the trial ended")
|
|
}
|
|
|
|
// 6. Verify the pool is back at rank 0 (default)
|
|
attachments, err = eq.GetActiveAttachmentsByPool(ctx, pool.PoolID)
|
|
if err != nil {
|
|
t.Fatalf("get active attachments: %v", err)
|
|
}
|
|
if len(attachments) != 1 {
|
|
t.Fatalf("expected 1 active attachment after expiry, got %d", len(attachments))
|
|
}
|
|
if attachments[0].ProductID != defProd.ProductID {
|
|
t.Fatalf("expected default product %s after expiry, got %s", defProd.ProductID, attachments[0].ProductID)
|
|
}
|
|
}
|