Replace the entity slugs on organizations, workspaces, resource pools, and plan ladders with nullable `key` columns and add keys to products, prices, and entitlement sets. Rename `providers.slug` to `provider` and add partial unique indexes for system and org role names. Assign invoice numbers per billing account from a gapless transactional counter; Stripe's number moves to the invoice mapping as an external reference. Seeds, fixtures, and the operator lookup address rows by key, and the returning-login resync no longer blanks a display name when the IdP sends no `name` claim.
960 lines
38 KiB
Go
960 lines
38 KiB
Go
package entitlements_test
|
|
|
|
// Doc 41 Appendix A proof scenarios (design D10, tasks 6.2) as integration
|
|
// tests over the Go conferral wrappers. Each test runs in a rolled-back
|
|
// transaction against the TEST_DATABASE_URL database (see testDB in
|
|
// materialize_test.go) and builds its own isolated fixture, so runs are
|
|
// deterministic and leave no rows behind.
|
|
//
|
|
// Also here: the confer timestamp regression test (zero ActivatedAt inside a
|
|
// long transaction must succeed; explicit future activation must be refused)
|
|
// and the vacancy-guarded baseline restoration tests over
|
|
// ReapplyDefaultsIfVacant (plan-enrollment-administration delta scenarios
|
|
// "Revoking the plan-holding grant restores the baseline" and "Restoration
|
|
// guard never supersedes another source").
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strings"
|
|
"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/organization"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// --- Local fixture helpers -------------------------------------------------
|
|
|
|
// proofLadder creates a plan ladder and appends the given products as tiers.
|
|
// CreatePlanLadderTier self-assigns rank MAX+1, so products land at ranks
|
|
// 0..N-1 in argument order.
|
|
func proofLadder(t *testing.T, ctx context.Context, tx *sql.Tx, key string, productIDs ...string) string {
|
|
t.Helper()
|
|
bq := billing.New(tx)
|
|
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
|
|
Name: "Doc41 " + key,
|
|
IsActive: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create ladder %s: %v", key, err)
|
|
}
|
|
for _, pid := range productIDs {
|
|
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
|
|
PlanLadderID: ladder.PlanLadderID,
|
|
ProductID: pid,
|
|
}); err != nil {
|
|
t.Fatalf("create tier on %s for %s: %v", key, pid, err)
|
|
}
|
|
}
|
|
return ladder.PlanLadderID
|
|
}
|
|
|
|
// proofGrant records a manual operator grant decree for the product.
|
|
func proofGrant(t *testing.T, ctx context.Context, tx *sql.Tx, to testOrg, productID, reason string) entitlements.Grant {
|
|
t.Helper()
|
|
q := entitlements.New(tx)
|
|
g, err := q.CreateGrant(ctx, entitlements.CreateGrantParams{
|
|
ProductID: productID,
|
|
GrantedToOrgID: uuid.NullUUID{UUID: uuid.MustParse(to.org.OrgID), Valid: true},
|
|
GrantedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(to.person.PersonID), Valid: true},
|
|
GrantReason: reason,
|
|
Quantity: 1,
|
|
ValidFrom: time.Now(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create grant for %s: %v", productID, err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// proofConfer confers the grant onto the org's pool, expecting the given
|
|
// outcome, and returns the provision id. The fixture person is the operator
|
|
// actor (chk_pool_provision_transitions_operator_actor requires an actor_id
|
|
// for operator-typed transitions).
|
|
func proofConfer(t *testing.T, ctx context.Context, tx *sql.Tx, to testOrg, productID string, grant entitlements.Grant, wantOutcome string) string {
|
|
t.Helper()
|
|
q := entitlements.New(tx)
|
|
provisionID, outcome, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: to.pool.PoolID,
|
|
ProductID: productID,
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
|
|
Quantity: 1,
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(to.person.PersonID), Valid: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("confer %s: %v", productID, err)
|
|
}
|
|
if outcome != wantOutcome {
|
|
t.Fatalf("confer %s outcome = %q, want %q", productID, outcome, wantOutcome)
|
|
}
|
|
return provisionID
|
|
}
|
|
|
|
// proofSubscription creates a billing account for the org and a bare core
|
|
// subscription row, returning the subscription id. Subscriptions are a
|
|
// writer-writable source table; only their enactment is enclosed.
|
|
func proofSubscription(t *testing.T, ctx context.Context, tx *sql.Tx, orgID string) uuid.UUID {
|
|
t.Helper()
|
|
bq := billing.New(tx)
|
|
account, err := bq.CreateBillingAccount(ctx, billing.CreateBillingAccountParams{
|
|
OrgID: orgID,
|
|
Name: "proof-" + uuid.New().String()[:8],
|
|
Status: "active",
|
|
Metadata: json.RawMessage("{}"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create billing account: %v", err)
|
|
}
|
|
var subID uuid.UUID
|
|
if err := tx.QueryRowContext(ctx,
|
|
`INSERT INTO core.subscriptions (billing_account_id, status) VALUES ($1, 'active')
|
|
RETURNING subscription_id`, account.BillingAccountID,
|
|
).Scan(&subID); err != nil {
|
|
t.Fatalf("create subscription: %v", err)
|
|
}
|
|
return subID
|
|
}
|
|
|
|
func proofCount(t *testing.T, ctx context.Context, tx *sql.Tx, query string, args ...any) int {
|
|
t.Helper()
|
|
var n int
|
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(&n); err != nil {
|
|
t.Fatalf("count query %q: %v", query, err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func proofActiveJunctions(t *testing.T, ctx context.Context, tx *sql.Tx, provisionID string) int {
|
|
return proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provision_ladders WHERE provision_id = $1 AND status = 'active'`,
|
|
provisionID)
|
|
}
|
|
|
|
func proofTransitions(t *testing.T, ctx context.Context, tx *sql.Tx, provisionID string) int {
|
|
return proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provision_transitions WHERE provision_id = $1`,
|
|
provisionID)
|
|
}
|
|
|
|
func proofProvisionStatus(t *testing.T, ctx context.Context, tx *sql.Tx, provisionID string) string {
|
|
t.Helper()
|
|
var s string
|
|
if err := tx.QueryRowContext(ctx,
|
|
`SELECT status FROM core.pool_provisions WHERE provision_id = $1`, provisionID,
|
|
).Scan(&s); err != nil {
|
|
t.Fatalf("provision status: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func proofBegin(t *testing.T) (context.Context, *sql.Tx) {
|
|
t.Helper()
|
|
database := testDB(t)
|
|
ctx := context.Background()
|
|
tx, err := database.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = tx.Rollback() })
|
|
return ctx, tx
|
|
}
|
|
|
|
// proofSavepoint runs fn inside a savepoint and rolls back to it, so a
|
|
// statement expected to fail doesn't abort the enclosing test transaction.
|
|
func proofSavepoint(t *testing.T, ctx context.Context, tx *sql.Tx, fn func()) {
|
|
t.Helper()
|
|
if _, err := tx.ExecContext(ctx, "SAVEPOINT proof_probe"); err != nil {
|
|
t.Fatalf("savepoint: %v", err)
|
|
}
|
|
fn()
|
|
if _, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT proof_probe"); err != nil {
|
|
t.Fatalf("rollback to savepoint: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Appendix A scenarios ----------------------------------------------------
|
|
|
|
// Scenario 1 — Addon granted, then revoked, over an active plan (finding #25).
|
|
// end_conferral resolves by source: revoking the off-ladder addon grant ends
|
|
// only the addon's provision and can never touch the plan's ladder position.
|
|
// The second revoke is refused at the decree level (RevokeGrant matches only
|
|
// status='active'), and a raw end_conferral replay is the quiet zero-live-
|
|
// target success, not an error.
|
|
func TestConferralProof_AddonRevokeReplay(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
|
|
plan := createTestProduct(t, ctx, tx, "p1-plan", 5, false)
|
|
proofLadder(t, ctx, tx, "p1", plan.productID)
|
|
planGrant := proofGrant(t, ctx, tx, to, plan.productID, "manual")
|
|
planProvision := proofConfer(t, ctx, tx, to, plan.productID, planGrant, "created")
|
|
|
|
addon := createTestProduct(t, ctx, tx, "p1-addon", 2, false)
|
|
addonGrant := proofGrant(t, ctx, tx, to, addon.productID, "promotional")
|
|
addonProvision := proofConfer(t, ctx, tx, to, addon.productID, addonGrant, "created")
|
|
|
|
// Revoke the addon: decree first, then end_conferral by source.
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: addonGrant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "proof-1", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("revoke addon decree: %v", err)
|
|
}
|
|
ended, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(addonGrant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("end addon conferral: %v", err)
|
|
}
|
|
if len(ended) != 1 || ended[0] != addonProvision {
|
|
t.Fatalf("ended = %v, want exactly [%s]", ended, addonProvision)
|
|
}
|
|
|
|
// The plan position is untouched.
|
|
if s := proofProvisionStatus(t, ctx, tx, planProvision); s != "active" {
|
|
t.Errorf("plan provision status = %q, want active", s)
|
|
}
|
|
if n := proofActiveJunctions(t, ctx, tx, planProvision); n != 1 {
|
|
t.Errorf("plan active junction rows = %d, want 1", n)
|
|
}
|
|
// The addon occupied no rung, so no position transition exists nor could
|
|
// be fabricated (transition_requires_occupancy).
|
|
if n := proofTransitions(t, ctx, tx, addonProvision); n != 0 {
|
|
t.Errorf("addon transitions = %d, want 0", n)
|
|
}
|
|
|
|
// Second revoke: refused at the decree level.
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: addonGrant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "proof-1-again", Valid: true},
|
|
}); err != sql.ErrNoRows {
|
|
t.Errorf("second revoke decree err = %v, want sql.ErrNoRows (decree-level refusal)", err)
|
|
}
|
|
// A raw source-arc replay is success with zero effect, never an error.
|
|
replay, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(addonGrant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("end_conferral replay: %v", err)
|
|
}
|
|
if len(replay) != 0 {
|
|
t.Errorf("replay ended %v, want empty set", replay)
|
|
}
|
|
}
|
|
|
|
// Scenario 2 — Plan granted to an org that already has it: the second decree
|
|
// is recorded (the grant is the ledger entry) and confer no-ops, appending no
|
|
// provision, junction, or transition row.
|
|
func TestConferralProof_SamePlanRegrantNoop(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
|
|
plan := createTestProduct(t, ctx, tx, "p2-plan", 5, false)
|
|
proofLadder(t, ctx, tx, "p2", plan.productID)
|
|
|
|
g1 := proofGrant(t, ctx, tx, to, plan.productID, "manual")
|
|
provision := proofConfer(t, ctx, tx, to, plan.productID, g1, "created")
|
|
|
|
g2 := proofGrant(t, ctx, tx, to, plan.productID, "manual")
|
|
noopProvision := proofConfer(t, ctx, tx, to, plan.productID, g2, "noop")
|
|
if noopProvision != provision {
|
|
t.Errorf("noop returned provision %s, want incumbent %s", noopProvision, provision)
|
|
}
|
|
|
|
// Both decrees stand as ledger rows.
|
|
if n := proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.grants WHERE product_id = $1 AND granted_to_org_id = $2`,
|
|
plan.productID, to.org.OrgID); n != 2 {
|
|
t.Errorf("grant rows = %d, want 2 (both decrees recorded)", n)
|
|
}
|
|
// No duplicated machinery.
|
|
if n := proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provisions WHERE pool_id = $1`, to.pool.PoolID); n != 1 {
|
|
t.Errorf("provisions = %d, want 1", n)
|
|
}
|
|
if n := proofActiveJunctions(t, ctx, tx, provision); n != 1 {
|
|
t.Errorf("junction rows = %d, want 1", n)
|
|
}
|
|
if n := proofTransitions(t, ctx, tx, provision); n != 1 {
|
|
t.Errorf("transitions = %d, want 1 (initiate only)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario 3 — The bare-entitlement-set grant form is structurally
|
|
// impossible: grants.product_id is NOT NULL and grants.entitlement_set_id no
|
|
// longer exists.
|
|
func TestConferralProof_BareSetGrantUnrepresentable(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
set := createTestProduct(t, ctx, tx, "p3-set", 1, false)
|
|
|
|
// A grant without a product is a NOT NULL violation.
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id, grant_reason)
|
|
VALUES (NULL, $1, $2, 'manual')`,
|
|
to.org.OrgID, to.person.PersonID)
|
|
if err == nil {
|
|
t.Fatal("expected NOT NULL violation inserting grant without product_id, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "product_id") {
|
|
t.Errorf("expected product_id not-null violation, got: %v", err)
|
|
}
|
|
})
|
|
|
|
// The bare-set column itself no longer exists.
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO core.grants (product_id, entitlement_set_id, granted_to_org_id, granted_by_person_id, grant_reason)
|
|
VALUES ($1, $2, $3, $4, 'manual')`,
|
|
set.productID, set.setID, to.org.OrgID, to.person.PersonID)
|
|
if err == nil {
|
|
t.Fatal("expected undefined-column error naming entitlement_set_id, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "entitlement_set_id") {
|
|
t.Errorf("expected entitlement_set_id undefined-column error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Scenario 4 — Tier rows AND a non-recurring price co-occur legally (a
|
|
// lifetime-license tier): product_shape reports the dimensions separately and
|
|
// confer grants the ladder position.
|
|
func TestConferralProof_LadderTierWithOneTimePrice(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
bq := billing.New(tx)
|
|
|
|
lifetime := createTestProduct(t, ctx, tx, "p4-lifetime", 5, false)
|
|
if _, err := bq.CreatePrice(ctx, billing.CreatePriceParams{
|
|
ProductID: lifetime.productID,
|
|
Currency: "usd",
|
|
UnitAmount: 19900,
|
|
// RecurringInterval left NULL: a one-time price.
|
|
}); err != nil {
|
|
t.Fatalf("create one-time price: %v", err)
|
|
}
|
|
proofLadder(t, ctx, tx, "p4", lifetime.productID)
|
|
|
|
var ladderCount int
|
|
var billingShape string
|
|
if err := tx.QueryRowContext(ctx,
|
|
`SELECT ladder_count, billing_shape FROM core.product_shape WHERE product_id = $1`,
|
|
lifetime.productID,
|
|
).Scan(&ladderCount, &billingShape); err != nil {
|
|
t.Fatalf("read product_shape: %v", err)
|
|
}
|
|
if ladderCount != 1 || billingShape != "one_time" {
|
|
t.Errorf("product_shape = (ladder_count=%d, billing_shape=%q), want (1, one_time)", ladderCount, billingShape)
|
|
}
|
|
|
|
grant := proofGrant(t, ctx, tx, to, lifetime.productID, "manual")
|
|
provision := proofConfer(t, ctx, tx, to, lifetime.productID, grant, "created")
|
|
if n := proofActiveJunctions(t, ctx, tx, provision); n != 1 {
|
|
t.Errorf("junction rows = %d, want 1 (position conferred despite one-time price)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario 5 — Prepaid credit pack, as far as the schema supports today: a
|
|
// one-time price with usage_type NULL is the legal shape (consumption lives
|
|
// in the set's per-unit rule and quantity), and the
|
|
// usage_pricing_requires_recurrence CHECK forbids putting a usage flavor on a
|
|
// non-recurring price.
|
|
func TestConferralProof_PrepaidCreditPackShape(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
bq := billing.New(tx)
|
|
|
|
// Per-unit set rule: 1 site per purchased unit — the credit flavor.
|
|
pack := createTestProduct(t, ctx, tx, "p5-pack", 1, true)
|
|
if _, err := bq.CreatePrice(ctx, billing.CreatePriceParams{
|
|
ProductID: pack.productID,
|
|
Currency: "usd",
|
|
UnitAmount: 500,
|
|
}); err != nil {
|
|
t.Fatalf("create one-time pack price: %v", err)
|
|
}
|
|
|
|
// A usage-typed non-recurring price is structurally rejected.
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO core.prices (product_id, currency, unit_amount, usage_type)
|
|
VALUES ($1, 'usd', 500, 'metered')`, pack.productID)
|
|
if err == nil {
|
|
t.Fatal("expected usage_pricing_requires_recurrence violation, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "usage_pricing_requires_recurrence") {
|
|
t.Errorf("expected chk_prices_usage_pricing_requires_recurrence, got: %v", err)
|
|
}
|
|
})
|
|
|
|
// Conferring the pack with quantity scales the per-unit entitlement.
|
|
grant, err := q.CreateGrant(ctx, entitlements.CreateGrantParams{
|
|
ProductID: pack.productID,
|
|
GrantedToOrgID: uuid.NullUUID{UUID: uuid.MustParse(to.org.OrgID), Valid: true},
|
|
GrantedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(to.person.PersonID), Valid: true},
|
|
GrantReason: "manual",
|
|
Quantity: 5,
|
|
ValidFrom: time.Now(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create pack grant: %v", err)
|
|
}
|
|
if _, _, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: to.pool.PoolID,
|
|
ProductID: pack.productID,
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
|
|
Quantity: 5,
|
|
ActorType: "operator",
|
|
}); err != nil {
|
|
t.Fatalf("confer pack: %v", err)
|
|
}
|
|
if err := entitlements.MaterializePoolEntitlements(ctx, q, to.pool.PoolID); err != nil {
|
|
t.Fatalf("materialize: %v", err)
|
|
}
|
|
if limit := getEntitlementLimit(t, ctx, q, to.pool.PoolID); limit != 5 {
|
|
t.Errorf("pack limit = %d, want 5 (1 per unit x qty 5)", limit)
|
|
}
|
|
}
|
|
|
|
// Scenario 6 — A product tiered into two ladders is conferred in ONE call:
|
|
// one provision, one junction row per ladder, one initiate transition per
|
|
// rung.
|
|
func TestConferralProof_TwoLadderConferral(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
|
|
dual := createTestProduct(t, ctx, tx, "p6-dual", 5, false)
|
|
ladder1 := proofLadder(t, ctx, tx, "p6-a", dual.productID)
|
|
ladder2 := proofLadder(t, ctx, tx, "p6-b", dual.productID)
|
|
|
|
grant := proofGrant(t, ctx, tx, to, dual.productID, "manual")
|
|
provision := proofConfer(t, ctx, tx, to, dual.productID, grant, "created")
|
|
|
|
if n := proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provisions WHERE pool_id = $1`, to.pool.PoolID); n != 1 {
|
|
t.Errorf("provisions = %d, want 1", n)
|
|
}
|
|
if n := proofActiveJunctions(t, ctx, tx, provision); n != 2 {
|
|
t.Errorf("active junction rows = %d, want 2 (one per ladder)", n)
|
|
}
|
|
for _, ladder := range []string{ladder1, ladder2} {
|
|
if n := proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provision_ladders
|
|
WHERE provision_id = $1 AND plan_ladder_id = $2 AND status = 'active'`,
|
|
provision, ladder); n != 1 {
|
|
t.Errorf("junction rows on ladder %s = %d, want 1", ladder, n)
|
|
}
|
|
}
|
|
if n := proofTransitions(t, ctx, tx, provision); n != 2 {
|
|
t.Errorf("transitions = %d, want 2 (initiate per rung)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario 7 — A cooperative with zero ladders: confer runs its off-ladder
|
|
// path — one provision, no junction, no transition, no ladder machinery
|
|
// consulted.
|
|
func TestConferralProof_NoLadderCooperative(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
|
|
product := createTestProduct(t, ctx, tx, "p7-flat", 3, false)
|
|
grant := proofGrant(t, ctx, tx, to, product.productID, "manual")
|
|
provision := proofConfer(t, ctx, tx, to, product.productID, grant, "created")
|
|
|
|
if s := proofProvisionStatus(t, ctx, tx, provision); s != "active" {
|
|
t.Errorf("provision status = %q, want active", s)
|
|
}
|
|
if n := proofCount(t, ctx, tx,
|
|
`SELECT count(*) FROM core.pool_provision_ladders WHERE provision_id = $1`, provision); n != 0 {
|
|
t.Errorf("junction rows = %d, want 0", n)
|
|
}
|
|
if n := proofTransitions(t, ctx, tx, provision); n != 0 {
|
|
t.Errorf("transitions = %d, want 0", n)
|
|
}
|
|
}
|
|
|
|
// Scenario 8 — Label/structure disagreement is unrepresentable by
|
|
// dissolution: display_category claims nothing, so 'addon' on a laddered
|
|
// product changes no conferral behavior — the position is conferred exactly
|
|
// as with a NULL label.
|
|
func TestConferralProof_DisplayCategoryInert(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
|
|
labeled := createTestProduct(t, ctx, tx, "p8-labeled", 5, false)
|
|
if _, err := tx.ExecContext(ctx,
|
|
`UPDATE core.products SET display_category = 'addon' WHERE product_id = $1`,
|
|
labeled.productID); err != nil {
|
|
t.Fatalf("set display_category: %v", err)
|
|
}
|
|
proofLadder(t, ctx, tx, "p8", labeled.productID)
|
|
|
|
grant := proofGrant(t, ctx, tx, to, labeled.productID, "manual")
|
|
provision := proofConfer(t, ctx, tx, to, labeled.productID, grant, "created")
|
|
|
|
if n := proofActiveJunctions(t, ctx, tx, provision); n != 1 {
|
|
t.Errorf("junction rows = %d, want 1 (label did not change position behavior)", n)
|
|
}
|
|
if n := proofTransitions(t, ctx, tx, provision); n != 1 {
|
|
t.Errorf("transitions = %d, want 1 (initiate recorded as for any tier product)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario 9 — One authoritative read path per consumer: conferral reads
|
|
// product_conferral_shapes (a draft product refuses conferral), diagnostics
|
|
// reads product_shape (mixed billing reported honestly in its own column).
|
|
func TestConferralProof_OneAuthoritativeSurface(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
bq := billing.New(tx)
|
|
|
|
// Draft product: representable in the catalog, refused by confer.
|
|
set, err := q.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
|
|
Name: "p9-draft-set-" + uuid.New().String()[:8], IsActive: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create draft set: %v", err)
|
|
}
|
|
var draftID string
|
|
if err := tx.QueryRowContext(ctx,
|
|
`INSERT INTO core.products (name, is_active, is_public, lifecycle_status, entitlement_set_id)
|
|
VALUES ('p9-draft', TRUE, FALSE, 'draft', $1) RETURNING product_id`, set.SetID,
|
|
).Scan(&draftID); err != nil {
|
|
t.Fatalf("create draft product: %v", err)
|
|
}
|
|
draftGrant := proofGrant(t, ctx, tx, to, draftID, "manual")
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
_, _, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: to.pool.PoolID,
|
|
ProductID: draftID,
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(draftGrant.GrantID), Valid: true},
|
|
Quantity: 1,
|
|
ActorType: "operator",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected confer to refuse a draft product, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "draft") {
|
|
t.Errorf("expected draft refusal, got: %v", err)
|
|
}
|
|
})
|
|
|
|
// Mixed billing: recurring + one-time active prices on one product is
|
|
// reported as 'mixed' in billing_shape, hidden behind no precedence rule.
|
|
mixed := createTestProduct(t, ctx, tx, "p9-mixed", 1, false)
|
|
if _, err := bq.CreatePrice(ctx, billing.CreatePriceParams{
|
|
ProductID: mixed.productID, Currency: "usd", UnitAmount: 1000,
|
|
RecurringInterval: sql.NullString{String: "month", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("create recurring price: %v", err)
|
|
}
|
|
if _, err := bq.CreatePrice(ctx, billing.CreatePriceParams{
|
|
ProductID: mixed.productID, Currency: "usd", UnitAmount: 9900,
|
|
}); err != nil {
|
|
t.Fatalf("create one-time price: %v", err)
|
|
}
|
|
var billingShape string
|
|
if err := tx.QueryRowContext(ctx,
|
|
`SELECT billing_shape FROM core.product_shape WHERE product_id = $1`,
|
|
mixed.productID).Scan(&billingShape); err != nil {
|
|
t.Fatalf("read product_shape: %v", err)
|
|
}
|
|
if billingShape != "mixed" {
|
|
t.Errorf("billing_shape = %q, want mixed", billingShape)
|
|
}
|
|
// The conferral surface reports the structural shape independently.
|
|
var ladderCount int
|
|
if err := tx.QueryRowContext(ctx,
|
|
`SELECT DISTINCT ladder_count FROM core.product_conferral_shapes WHERE product_id = $1`,
|
|
mixed.productID).Scan(&ladderCount); err != nil {
|
|
t.Fatalf("read product_conferral_shapes: %v", err)
|
|
}
|
|
if ladderCount != 0 {
|
|
t.Errorf("conferral ladder_count = %d, want 0 (no tiers)", ladderCount)
|
|
}
|
|
}
|
|
|
|
// Scenario 10 — grant_reason is a closed classification: out-of-domain values
|
|
// cannot reach the column, and 'default' is structurally system-authored (the
|
|
// biconditional CHECK, in both directions).
|
|
func TestConferralProof_GrantReasonDomain(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
to := setupTestOrg(t, ctx, tx)
|
|
product := createTestProduct(t, ctx, tx, "p10", 1, false)
|
|
|
|
insertGrant := func(reason string, byPerson bool) error {
|
|
grantedBy := sql.NullString{}
|
|
if byPerson {
|
|
grantedBy = sql.NullString{String: to.person.PersonID, Valid: true}
|
|
}
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id, grant_reason)
|
|
VALUES ($1, $2, $3, $4)`,
|
|
product.productID, to.org.OrgID, grantedBy, reason)
|
|
return err
|
|
}
|
|
expectViolation := func(reason string, byPerson bool, wantConstraint string) {
|
|
t.Helper()
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
if err := insertGrant(reason, byPerson); err == nil {
|
|
t.Errorf("expected %s violation for reason %q, got nil", wantConstraint, reason)
|
|
} else if !strings.Contains(err.Error(), wantConstraint) {
|
|
t.Errorf("expected %s for reason %q, got: %v", wantConstraint, reason, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The retired value 'trial' is outside the domain; free text cannot
|
|
// reach the reason column.
|
|
expectViolation("trial", true, "chk_grants_reason_domain")
|
|
expectViolation("special favor", true, "chk_grants_reason_domain")
|
|
// The biconditional, in both directions: operator-typed 'default' and a
|
|
// system-authored grant claiming anything else.
|
|
expectViolation("default", true, "chk_grants_default_iff_system_authored")
|
|
expectViolation("manual", false, "chk_grants_default_iff_system_authored")
|
|
|
|
// The legal shapes on both sides of the biconditional still insert.
|
|
if err := insertGrant("evaluation", true); err != nil {
|
|
t.Errorf("operator 'evaluation' grant should insert: %v", err)
|
|
}
|
|
if err := insertGrant("default", false); err != nil {
|
|
t.Errorf("system-authored 'default' grant should insert: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Confer timestamp regression ---------------------------------------------
|
|
|
|
// Regression for the transaction-timestamp bug: a Confer with zero
|
|
// ActivatedAt marshals as SQL NULL (database-resolved "now"), so it must
|
|
// succeed inside a transaction that has already done prior work — the
|
|
// Go-sampled wall clock is always ahead of the frozen transaction_timestamp
|
|
// and used to be rejected as "in the future". An explicitly future
|
|
// activation must still be refused.
|
|
func TestConfer_TimestampSemantics(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
// The fixture itself is the "prior statements in this transaction" load.
|
|
to := setupTestOrg(t, ctx, tx)
|
|
product := createTestProduct(t, ctx, tx, "reg-ts", 1, false)
|
|
grant := proofGrant(t, ctx, tx, to, product.productID, "manual")
|
|
|
|
// Explicit future activation is refused (probed first, inside a
|
|
// savepoint, so the raise doesn't abort the test transaction).
|
|
proofSavepoint(t, ctx, tx, func() {
|
|
_, _, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: to.pool.PoolID,
|
|
ProductID: product.productID,
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
|
|
Quantity: 1,
|
|
ActorType: "operator",
|
|
ActivatedAt: time.Now().Add(time.Hour),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected future-activation refusal, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "future") {
|
|
t.Errorf("expected future-activation error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
if _, outcome, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: to.pool.PoolID,
|
|
ProductID: product.productID,
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
|
|
Quantity: 1,
|
|
ActorType: "operator",
|
|
// ActivatedAt zero: NULL-means-now.
|
|
}); err != nil {
|
|
t.Fatalf("confer with zero ActivatedAt inside an active tx: %v", err)
|
|
} else if outcome != "created" {
|
|
t.Fatalf("outcome = %q, want created", outcome)
|
|
}
|
|
}
|
|
|
|
// --- Vacancy-guarded baseline restoration ------------------------------------
|
|
|
|
// defaultLadderFixture wires an org whose org type (mutated inside the
|
|
// rolled-back tx) configures a default ladder with a rank-0 baseline product
|
|
// and a rank-1 plan product.
|
|
type defaultLadderFixture struct {
|
|
to testOrg
|
|
ladderID string
|
|
baseline testProduct
|
|
plan testProduct
|
|
}
|
|
|
|
func setupDefaultLadderFixture(t *testing.T, ctx context.Context, tx *sql.Tx) defaultLadderFixture {
|
|
t.Helper()
|
|
to := setupTestOrg(t, ctx, tx)
|
|
baseline := createTestProduct(t, ctx, tx, "restore-base", 1, false)
|
|
plan := createTestProduct(t, ctx, tx, "restore-plan", 5, false)
|
|
ladderID := proofLadder(t, ctx, tx, "restore", baseline.productID, plan.productID)
|
|
|
|
oq := organization.New(tx)
|
|
if _, err := oq.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{
|
|
OrgType: "personal",
|
|
DefaultPlanLadderID: uuid.NullUUID{UUID: uuid.MustParse(ladderID), Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("set org-type default ladder: %v", err)
|
|
}
|
|
return defaultLadderFixture{to: to, ladderID: ladderID, baseline: baseline, plan: plan}
|
|
}
|
|
|
|
// Revoking the plan-holding grant leaves the default ladder vacant, so the
|
|
// guarded restoration confers a system-authored default decree returning the
|
|
// pool to the rank-0 tier.
|
|
func TestReapplyDefaultsIfVacant_RestoresVacantDefaultLadder(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
f := setupDefaultLadderFixture(t, ctx, tx)
|
|
|
|
grant := proofGrant(t, ctx, tx, f.to, f.plan.productID, "manual")
|
|
proofConfer(t, ctx, tx, f.to, f.plan.productID, grant, "created")
|
|
|
|
// Decree-first revoke, then end by source: the ladder is left vacant.
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: grant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "restore-test", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("revoke decree: %v", err)
|
|
}
|
|
if _, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("end conferral: %v", err)
|
|
}
|
|
|
|
res, err := entitlements.ReapplyDefaultsIfVacant(ctx, tx, f.to.pool.PoolID, entitlements.Actor{
|
|
ActorType: "system", Reason: "restore-test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReapplyDefaultsIfVacant: %v", err)
|
|
}
|
|
if res.Outcome != "created" {
|
|
t.Fatalf("restoration outcome = %q, want created", res.Outcome)
|
|
}
|
|
|
|
// The pool is back on the rank-0 baseline, delivered by a system-authored
|
|
// default decree.
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.pool_provision_ladders l
|
|
JOIN core.pool_provisions p ON p.provision_id = l.provision_id
|
|
WHERE l.pool_id = $1 AND l.plan_ladder_id = $2 AND l.status = 'active'
|
|
AND p.product_id = $3`,
|
|
f.to.pool.PoolID, f.ladderID, f.baseline.productID); n != 1 {
|
|
t.Errorf("active rank-0 baseline attachments = %d, want 1", n)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.grants
|
|
WHERE granted_to_org_id = $1 AND grant_reason = 'default' AND granted_by_person_id IS NULL`,
|
|
f.to.org.OrgID); n != 1 {
|
|
t.Errorf("system-authored default decrees = %d, want 1", n)
|
|
}
|
|
}
|
|
|
|
// Ending an off-ladder grant while a subscription-sourced provision holds the
|
|
// default ladder finds the ladder occupied: the guard does nothing and never
|
|
// supersedes the other source.
|
|
func TestReapplyDefaultsIfVacant_NeverSupersedesAnotherSource(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
f := setupDefaultLadderFixture(t, ctx, tx)
|
|
|
|
// A subscription holds the pool's position on the default ladder.
|
|
subID := proofSubscription(t, ctx, tx, f.to.org.OrgID)
|
|
subProvision, outcome, err := q.Confer(ctx, entitlements.ConferParams{
|
|
PoolID: f.to.pool.PoolID,
|
|
ProductID: f.plan.productID,
|
|
SubscriptionID: uuid.NullUUID{UUID: subID, Valid: true},
|
|
Quantity: 1,
|
|
ActorType: "webhook",
|
|
})
|
|
if err != nil || outcome != "created" {
|
|
t.Fatalf("confer subscription plan: outcome=%q err=%v", outcome, err)
|
|
}
|
|
|
|
// An unrelated off-ladder grant is issued and then revoked.
|
|
addon := createTestProduct(t, ctx, tx, "restore-addon", 2, false)
|
|
addonGrant := proofGrant(t, ctx, tx, f.to, addon.productID, "promotional")
|
|
proofConfer(t, ctx, tx, f.to, addon.productID, addonGrant, "created")
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: addonGrant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "guard-test", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("revoke addon decree: %v", err)
|
|
}
|
|
if _, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(addonGrant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
}); err != nil {
|
|
t.Fatalf("end addon conferral: %v", err)
|
|
}
|
|
|
|
res, err := entitlements.ReapplyDefaultsIfVacant(ctx, tx, f.to.pool.PoolID, entitlements.Actor{
|
|
ActorType: "system", Reason: "guard-test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReapplyDefaultsIfVacant: %v", err)
|
|
}
|
|
if !res.AlreadyAtTier || res.Outcome != "noop" {
|
|
t.Fatalf("guard result = %+v, want noop (ladder occupied by subscription)", res)
|
|
}
|
|
|
|
// The subscription's position is untouched and no default decree exists.
|
|
if s := proofProvisionStatus(t, ctx, tx, subProvision); s != "active" {
|
|
t.Errorf("subscription provision status = %q, want active", s)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.grants
|
|
WHERE granted_to_org_id = $1 AND grant_reason = 'default'`,
|
|
f.to.org.OrgID); n != 0 {
|
|
t.Errorf("default decrees = %d, want 0 (guard must not restore)", n)
|
|
}
|
|
}
|
|
|
|
// A pool whose only live plan position sits on a NON-default ladder (e.g. a
|
|
// grandfathered legacy grant) is occupied under the floor guard: ending an
|
|
// unrelated off-ladder grant restores nothing, and the pool never accretes
|
|
// the current default alongside its existing plan (org-type-default-change-flow
|
|
// delta scenarios for plan-enrollment-administration).
|
|
func TestReapplyDefaultsIfVacant_DefersToPlanOnNonDefaultLadder(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
f := setupDefaultLadderFixture(t, ctx, tx)
|
|
|
|
// The pool's plan lives on a second ladder that is not the org-type default.
|
|
legacyPlan := createTestProduct(t, ctx, tx, "floor-legacy", 3, false)
|
|
proofLadder(t, ctx, tx, "floor-legacy", legacyPlan.productID)
|
|
legacyGrant := proofGrant(t, ctx, tx, f.to, legacyPlan.productID, "legacy")
|
|
legacyProvision := proofConfer(t, ctx, tx, f.to, legacyPlan.productID, legacyGrant, "created")
|
|
|
|
// An unrelated off-ladder grant is issued and then revoked.
|
|
addon := createTestProduct(t, ctx, tx, "floor-addon", 2, false)
|
|
addonGrant := proofGrant(t, ctx, tx, f.to, addon.productID, "promotional")
|
|
proofConfer(t, ctx, tx, f.to, addon.productID, addonGrant, "created")
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: addonGrant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "floor-guard-test", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("revoke addon decree: %v", err)
|
|
}
|
|
if _, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(addonGrant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("end addon conferral: %v", err)
|
|
}
|
|
|
|
res, err := entitlements.ReapplyDefaultsIfVacant(ctx, tx, f.to.pool.PoolID, entitlements.Actor{
|
|
ActorType: "system", Reason: "floor-guard-test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReapplyDefaultsIfVacant: %v", err)
|
|
}
|
|
if !res.AlreadyAtTier || res.Outcome != "noop" {
|
|
t.Fatalf("guard result = %+v, want noop (pool occupied on a non-default ladder)", res)
|
|
}
|
|
|
|
// The legacy position is untouched, and the default ladder gained nothing:
|
|
// no union of old plan + current default.
|
|
if s := proofProvisionStatus(t, ctx, tx, legacyProvision); s != "active" {
|
|
t.Errorf("legacy provision status = %q, want active", s)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.pool_provision_ladders
|
|
WHERE pool_id = $1 AND plan_ladder_id = $2`,
|
|
f.to.pool.PoolID, f.ladderID); n != 0 {
|
|
t.Errorf("default-ladder attachments = %d, want 0 (floor guard must not layer the default)", n)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.grants
|
|
WHERE granted_to_org_id = $1 AND grant_reason = 'default'`,
|
|
f.to.org.OrgID); n != 0 {
|
|
t.Errorf("default decrees = %d, want 0 (guard must not restore)", n)
|
|
}
|
|
}
|
|
|
|
// The grandfather sunset, end to end: an org whose plan is held by a legacy
|
|
// grant on a NON-default ladder has that grant revoked — the floor guard then
|
|
// finds the pool with no live plan and converges it onto the CURRENT default,
|
|
// on a different ladder than the one the clause preserved
|
|
// (org-type-default-change-flow: "Grandfather clause can be sunset").
|
|
func TestReapplyDefaultsIfVacant_GrandfatherSunsetConvergesToCurrentDefault(t *testing.T) {
|
|
ctx, tx := proofBegin(t)
|
|
q := entitlements.New(tx)
|
|
f := setupDefaultLadderFixture(t, ctx, tx) // current default: ladder A, rank-0 = f.baseline
|
|
|
|
// Grandfathered state: a legacy grant holds the pool's only plan, on a
|
|
// second ladder that is not the org-type default.
|
|
legacyPlan := createTestProduct(t, ctx, tx, "sunset-legacy", 3, false)
|
|
proofLadder(t, ctx, tx, "sunset-legacy", legacyPlan.productID)
|
|
legacyGrant := proofGrant(t, ctx, tx, f.to, legacyPlan.productID, "legacy")
|
|
legacyProvision := proofConfer(t, ctx, tx, f.to, legacyPlan.productID, legacyGrant, "created")
|
|
|
|
// Sunset: the operator revokes the legacy grant (decree-first), its
|
|
// conferral ends by source, and the guarded restoration runs — the pool
|
|
// is left with no live plan, so the current default is conferred.
|
|
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{
|
|
GrantID: legacyGrant.GrantID,
|
|
RevokedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
RevocationReason: sql.NullString{String: "sunset-test", Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("revoke legacy decree: %v", err)
|
|
}
|
|
if _, err := q.EndConferral(ctx, entitlements.EndConferralParams{
|
|
GrantID: uuid.NullUUID{UUID: uuid.MustParse(legacyGrant.GrantID), Valid: true},
|
|
ActorType: "operator",
|
|
ActorID: uuid.NullUUID{UUID: uuid.MustParse(f.to.person.PersonID), Valid: true},
|
|
}); err != nil {
|
|
t.Fatalf("end legacy conferral: %v", err)
|
|
}
|
|
res, err := entitlements.ReapplyDefaultsIfVacant(ctx, tx, f.to.pool.PoolID, entitlements.Actor{
|
|
ActorType: "system", Reason: "sunset-test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReapplyDefaultsIfVacant: %v", err)
|
|
}
|
|
if res.Outcome != "created" {
|
|
t.Fatalf("restoration outcome = %q, want created (clause ended, pool converges)", res.Outcome)
|
|
}
|
|
|
|
// The legacy position is gone and the pool sits on the current default's
|
|
// rank-0 — not on the ladder the clause used to preserve.
|
|
if s := proofProvisionStatus(t, ctx, tx, legacyProvision); s != "ended" {
|
|
t.Errorf("legacy provision status = %q, want ended", s)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.pool_provision_ladders l
|
|
JOIN core.pool_provisions p ON p.provision_id = l.provision_id
|
|
WHERE l.pool_id = $1 AND l.plan_ladder_id = $2 AND l.status = 'active'
|
|
AND p.product_id = $3`,
|
|
f.to.pool.PoolID, f.ladderID, f.baseline.productID); n != 1 {
|
|
t.Errorf("current-default attachments = %d, want 1 (converged)", n)
|
|
}
|
|
if n := proofCount(t, ctx, tx, `
|
|
SELECT count(*) FROM core.grants
|
|
WHERE granted_to_org_id = $1 AND grant_reason = 'default' AND granted_by_person_id IS NULL`,
|
|
f.to.org.OrgID); n != 1 {
|
|
t.Errorf("system default decrees = %d, want 1", n)
|
|
}
|
|
}
|