Give the demo's add-ons an entitlement set
The demo seed's three products on no ladder (demo-addon, demo-usage,
demo-onetime) had no entitlement set. Since the previous commit the Add
tier picker leaves out a product with no set, so the demo ladder had
nothing to add: the screen capture never rendered the tier add form, and
TestEveryRegisteredFormWasCaptured failed once make screens wrote a new
capture list. That commit's suite ran before the capture and did not see
it.
The add-ons now share a second demo set, demo_addons ("demo-addons"),
with the same two rules as demo_baseline (a limit of 5 on demo.widgets,
a boolean on demo.feature-x), created and found by its key like the
first and never written over. An add-on with no set is also #272's
fault in the demo's own data: a member could buy one and receive
nothing.
The seed golden, the demo README's catalog and counts, the identifiers
page and the demo seed spec's catalog requirement (a delta in
tiers-need-an-entitlement-set) say two sets.
make screens: the ladder page's open and invalid captures show the Add
tier form again, with the add-ons in its list; the Entitlement sets list
and the products list show demo-addons; the add-ons' product page reads
ready to grant. TestEveryRegisteredFormWasCaptured passes after the
capture. The suite ran on this tree with the next commit's migration;
TestPlanLaddersWalkthrough timed out once waiting for its create toast
(#222) and passed twice alone.
This commit is contained in:
+1
-1
@@ -126,7 +126,7 @@ legitimate; the product forms warn about duplicates instead.
|
||||
now holds it; ensuring existence makes that harmless where updating would
|
||||
silently overwrite an operator's edits. A declarative loader whose file is
|
||||
the truth may update, and must say so; none exists.
|
||||
- The demo seed keys its rows `demo_ladder`, `demo_baseline`, `demo_plan`,
|
||||
- The demo seed keys its rows `demo_ladder`, `demo_baseline`, `demo_addons`, `demo_plan`,
|
||||
`demo_plan_2`, `demo_plan_3`, `demo_addon`, `demo_usage`, `demo_onetime`.
|
||||
The mockshot sample data keys its organizations `mockshot_0001` onward and
|
||||
marks its persons with `mockshot:`-prefixed OIDC subjects; the nuke deletes
|
||||
|
||||
@@ -95,6 +95,9 @@ const (
|
||||
setKey = "demo_baseline"
|
||||
setName = "demo-baseline"
|
||||
|
||||
addonSetKey = "demo_addons"
|
||||
addonSetName = "demo-addons"
|
||||
|
||||
ladderKey = "demo_ladder"
|
||||
ladderName = "Demo Ladder"
|
||||
|
||||
@@ -155,7 +158,13 @@ func Seed(ctx context.Context, database *sql.DB, logger *slog.Logger) (Summary,
|
||||
}
|
||||
s.note(created)
|
||||
|
||||
productIDs, productSummary, err := seedProducts(ctx, billQ, setID, logger)
|
||||
addonSetID, created, err := seedAddonSet(ctx, database, entQ, logger)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("seed add-on entitlement set: %w", err)
|
||||
}
|
||||
s.note(created)
|
||||
|
||||
productIDs, productSummary, err := seedProducts(ctx, billQ, setID, addonSetID, logger)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("seed products: %w", err)
|
||||
}
|
||||
@@ -305,7 +314,47 @@ func SeedOperator(ctx context.Context, database *sql.DB, logger *slog.Logger) (b
|
||||
return seedPerson(ctx, database, identity.New(database), operatorPerson, logger)
|
||||
}
|
||||
|
||||
// demoSet describes one demo entitlement set: its key, display name,
|
||||
// description, and the two rules it carries (a limit on demo.widgets and a
|
||||
// boolean on demo.feature-x).
|
||||
type demoSet struct {
|
||||
key, name, description string
|
||||
widgets int64
|
||||
widgetsText, flagText string
|
||||
}
|
||||
|
||||
var (
|
||||
baselineSet = demoSet{
|
||||
key: setKey,
|
||||
name: setName,
|
||||
description: "Demo baseline entitlement set seeded by member-console seed-demo.",
|
||||
widgets: 10,
|
||||
widgetsText: "Up to 10 demo widgets.",
|
||||
flagText: "Feature X enabled for demo grants.",
|
||||
}
|
||||
addonSet = demoSet{
|
||||
key: addonSetKey,
|
||||
name: addonSetName,
|
||||
description: "Demo add-on entitlement set seeded by member-console seed-demo.",
|
||||
widgets: 5,
|
||||
widgetsText: "Up to 5 more demo widgets.",
|
||||
flagText: "Feature X enabled by a demo add-on.",
|
||||
}
|
||||
)
|
||||
|
||||
// seedEntitlementSet ensures the `demo_baseline` set and its two rules exist.
|
||||
func seedEntitlementSet(ctx context.Context, database *sql.DB, q *entitlements.Queries, logger *slog.Logger) (string, bool, error) {
|
||||
return ensureDemoSet(ctx, database, q, baselineSet, logger)
|
||||
}
|
||||
|
||||
// seedAddonSet ensures the `demo_addons` set and its two rules exist. The
|
||||
// demo's add-on, usage, and one-time products link to it, so each delivers
|
||||
// something when a member buys it.
|
||||
func seedAddonSet(ctx context.Context, database *sql.DB, q *entitlements.Queries, logger *slog.Logger) (string, bool, error) {
|
||||
return ensureDemoSet(ctx, database, q, addonSet, logger)
|
||||
}
|
||||
|
||||
// ensureDemoSet ensures one demo set and its two rules exist.
|
||||
// Read by key, create with the key when absent, leave an existing set's own
|
||||
// name, description, and active flag untouched (entity-keys §9 A1).
|
||||
//
|
||||
@@ -313,28 +362,28 @@ func SeedOperator(ctx context.Context, database *sql.DB, logger *slog.Logger) (b
|
||||
// plus the resource key it constrains (entity-keys §3, "a child that is a
|
||||
// singleton by construction has no key"), so the ensure-if-absent pass below
|
||||
// also recovers a set left rule-less by an aborted earlier run.
|
||||
func seedEntitlementSet(ctx context.Context, database *sql.DB, q *entitlements.Queries, logger *slog.Logger) (string, bool, error) {
|
||||
func ensureDemoSet(ctx context.Context, database *sql.DB, q *entitlements.Queries, d demoSet, logger *slog.Logger) (string, bool, error) {
|
||||
var setID string
|
||||
existing, err := q.GetEntitlementSetByKey(ctx, nullString(setKey))
|
||||
existing, err := q.GetEntitlementSetByKey(ctx, nullString(d.key))
|
||||
existed := err == nil
|
||||
switch {
|
||||
case existed:
|
||||
setID = existing.SetID
|
||||
logger.Info("demoseed: entitlement set exists, ensuring rules", slog.String("key", setKey), slog.String("set_id", setID))
|
||||
logger.Info("demoseed: entitlement set exists, ensuring rules", slog.String("key", d.key), slog.String("set_id", setID))
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
set, createErr := q.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
|
||||
Key: nullString(setKey),
|
||||
Name: setName,
|
||||
Description: nullString("Demo baseline entitlement set seeded by member-console seed-demo."),
|
||||
Key: nullString(d.key),
|
||||
Name: d.name,
|
||||
Description: nullString(d.description),
|
||||
IsActive: true,
|
||||
})
|
||||
if createErr != nil {
|
||||
return "", false, fmt.Errorf("create entitlement set: %w", createErr)
|
||||
}
|
||||
setID = set.SetID
|
||||
logger.Info("demoseed: created entitlement set", slog.String("key", setKey), slog.String("set_id", setID))
|
||||
logger.Info("demoseed: created entitlement set", slog.String("key", d.key), slog.String("set_id", setID))
|
||||
default:
|
||||
return "", false, fmt.Errorf("lookup entitlement set %s: %w", setKey, err)
|
||||
return "", false, fmt.Errorf("lookup entitlement set %s: %w", d.key, err)
|
||||
}
|
||||
|
||||
rules, err := q.GetActiveRulesBySetID(ctx, setID)
|
||||
@@ -357,10 +406,10 @@ func seedEntitlementSet(ctx context.Context, database *sql.DB, q *entitlements.Q
|
||||
if err := addSeedRule(ctx, database, setID, entitlements.RuleFields{
|
||||
RuleType: "limit",
|
||||
ResourceKey: nullString("demo.widgets"),
|
||||
ResourceValue: sql.NullInt64{Int64: 10, Valid: true},
|
||||
ResourceValue: sql.NullInt64{Int64: d.widgets, Valid: true},
|
||||
ResourcePerUnit: sql.NullBool{Bool: false, Valid: true},
|
||||
StackingPolicy: nullString("additive"),
|
||||
Description: nullString("Up to 10 demo widgets."),
|
||||
Description: nullString(d.widgetsText),
|
||||
}); err != nil {
|
||||
return "", false, fmt.Errorf("create rule (limit): %w", err)
|
||||
}
|
||||
@@ -370,7 +419,7 @@ func seedEntitlementSet(ctx context.Context, database *sql.DB, q *entitlements.Q
|
||||
if err := addSeedRule(ctx, database, setID, entitlements.RuleFields{
|
||||
RuleType: "boolean",
|
||||
ResourceKey: nullString("demo.feature-x"),
|
||||
Description: nullString("Feature X enabled for demo grants."),
|
||||
Description: nullString(d.flagText),
|
||||
}); err != nil {
|
||||
return "", false, fmt.Errorf("create rule (boolean): %w", err)
|
||||
}
|
||||
@@ -397,19 +446,21 @@ func addSeedRule(ctx context.Context, database *sql.DB, setID string, rule entit
|
||||
|
||||
// seedProducts ensures six demo products exist and returns the
|
||||
// database-generated product ID for each key. Plan-typed products link to the
|
||||
// demo_baseline entitlement set so the plan ladder can drive materialization.
|
||||
// demo_baseline entitlement set so the plan ladder can drive materialization;
|
||||
// the add-on, usage, and one-time products share the demo_addons set, so none
|
||||
// is a product that delivers nothing and the tier picker offers them.
|
||||
//
|
||||
// A key, not a name, is what addresses these rows: core.products.name carries
|
||||
// no uniqueness constraint (two ladders may each have a "Pro"), so a
|
||||
// name-matched seed would either find the wrong row or create a second one. A
|
||||
// product that already carries the key is returned as it stands; the seed
|
||||
// never writes over an operator's edits (entity-keys §9 A1).
|
||||
func seedProducts(ctx context.Context, q *billing.Queries, setID string, logger *slog.Logger) (map[string]string, Summary, error) {
|
||||
func seedProducts(ctx context.Context, q *billing.Queries, setID, addonSetID string, logger *slog.Logger) (map[string]string, Summary, error) {
|
||||
var s Summary
|
||||
ids := make(map[string]string, 6)
|
||||
|
||||
planSet := uuid.NullUUID{UUID: uuid.MustParse(setID), Valid: true}
|
||||
noSet := uuid.NullUUID{}
|
||||
addonSetRef := uuid.NullUUID{UUID: uuid.MustParse(addonSetID), Valid: true}
|
||||
|
||||
// Plan products: display_category is NULL -- "plan" is derived structurally
|
||||
// from plan_ladder_tier membership; display_category is presentation-only.
|
||||
@@ -423,9 +474,9 @@ func seedProducts(ctx context.Context, q *billing.Queries, setID string, logger
|
||||
{keyPlan, "demo-plan", sql.NullString{}, planSet, "Demo plan product (rank 0)."},
|
||||
{keyPlan2, "demo-plan-2", sql.NullString{}, planSet, "Demo plan product (rank 1)."},
|
||||
{keyPlan3, "demo-plan-3", sql.NullString{}, planSet, "Demo plan product (rank 2)."},
|
||||
{keyAddon, "demo-addon", nullString("addon"), noSet, "Demo addon product."},
|
||||
{keyUsage, "demo-usage", nullString("usage"), noSet, "Demo usage-based product."},
|
||||
{keyOnetime, "demo-onetime", nullString("one_time"), noSet, "Demo one-time product."},
|
||||
{keyAddon, "demo-addon", nullString("addon"), addonSetRef, "Demo addon product."},
|
||||
{keyUsage, "demo-usage", nullString("usage"), addonSetRef, "Demo usage-based product."},
|
||||
{keyOnetime, "demo-onetime", nullString("one_time"), addonSetRef, "Demo one-time product."},
|
||||
}
|
||||
|
||||
for _, p := range products {
|
||||
|
||||
@@ -53,7 +53,11 @@ func seedCatalog(t *testing.T, plain *sql.DB) string {
|
||||
if err != nil {
|
||||
t.Fatalf("seed entitlement set: %v", err)
|
||||
}
|
||||
ids, _, err := seedProducts(ctx, billing.New(plain), setID, logger)
|
||||
addonSetID, _, err := seedAddonSet(ctx, plain, entitlements.New(plain), logger)
|
||||
if err != nil {
|
||||
t.Fatalf("seed add-on entitlement set: %v", err)
|
||||
}
|
||||
ids, _, err := seedProducts(ctx, billing.New(plain), setID, addonSetID, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("seed products: %v", err)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
first: operator created true; seed created 15, skipped 1
|
||||
second: operator created false; seed created 0, skipped 16
|
||||
first: operator created true; seed created 16, skipped 1
|
||||
second: operator created false; seed created 0, skipped 17
|
||||
|
||||
-- rows that differ
|
||||
none
|
||||
|
||||
@@ -35,6 +35,7 @@ Listed here so the change carries them openly (`CONTRIBUTING.md`, "How changes a
|
||||
|
||||
- **The forms library's `Slot` doc comment** (`internal/forms/forms.go:614-617`) says a slot may also hold a line the handler decides per render (the product's Entitlement set); a comment, no behaviour.
|
||||
- **The tier picker hides products with no set**, with the filter the grant form already applies (`loadIssuanceProducts`, `operator_enrollment.go`), in `tierCandidates`, which `availableTierProducts` calls.
|
||||
- **The demo seed's add-ons gain an entitlement set.** The demo's three products on no ladder (`demo-addon`, `demo-usage`, `demo-onetime`) had no set, so the tier picker left the demo ladder nothing to add and the screen capture never rendered the Add tier form. They now carry a second demo set, `demo_addons` (`internal/demoseed/demoseed.go`), with the same two rules as `demo_baseline`; the seed's README and the seed golden follow.
|
||||
- **The fixture sweep, ten files.** Nine test files make a product with no set a tier, and the trigger refuses each one. Seven build the product without a set: `internal/db/plan_act_obligations_test.go`, `internal/db/acts_test.go`, `internal/entitlements/plan_act_queries_db_test.go`, `internal/server/member_switch_offer_db_test.go`, `internal/server/member_extras_test.go`, `internal/server/operator_product_name_check_test.go` and `test/e2e/merchant-disclosure/sheet_test.go`. `internal/server/plan_acts_golden_db_test.go` clears a product's set and then ladders it (`setless()`), and `internal/entitlements/grant_resumption_test.go` clears the set of a tier. Each gets a set, or moves its failure to a draft lifecycle (below). The tenth is the test kit's `internal/testkit/world/world.go`, whose `WithoutSet` doc comment says the edit form clears any product's set; it gains that a tier's set cannot be cleared. Once the trigger is in place, `make test` names any other case by the constraint.
|
||||
- **The failure the plan-act goldens inject.** Four goldens of `internal/server/testdata/plan_acts/` (`default-initiate-fails`, `default-migrate-restore-fails`, `remove-second-pool-fails`, `reorder-an-organization-fails`) and `TestResumption_ARefusedConferralIsRolledBackAlone` use a rank-0 product with no set as the conferral that fails. They move to a product that is made draft after it is laddered, which `core.confer` also refuses. The failure step is the same; the recorded driver text changes.
|
||||
|
||||
@@ -60,6 +61,7 @@ None.
|
||||
|
||||
- `plan-ladder-management`: added "Every plan tier's product has an entitlement set" (the two triggers, the lock, and the rows they leave alone); modified "Operator tier management within a ladder" (the picker offers only products with a set, and the tier add's refusal).
|
||||
- `product-management`: modified "Operator product editing" ("None" is not offered for a tier product and the placeholder takes its place, the field's line, the stale page's refusal, and the failed read).
|
||||
- `member-console-demo-seed`: modified "Reference catalog shape" (a second demo set, `demo_addons`, for the three products on no ladder; the Entitlement Sets tab shows two sets).
|
||||
|
||||
## Impact
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Reference catalog shape
|
||||
|
||||
The seeder SHALL insert the following rows on first invocation against an empty database, each addressed by its `key` (`ui-vocabulary`; migration `00012_entity_keys`): the seeder names the key, the database generates the id, and the seeder reads that id back for the rows that reference it. The seeder SHALL NOT supply, compute, or pin an id for any row, and SHALL NOT find a row by its `name` or by a marker column.
|
||||
|
||||
- Two entitlement sets, each with the same two rules (one resource cap, one flag): `demo_baseline`, named "demo-baseline", for the plan products, and `demo_addons`, named "demo-addons", for the three products on no ladder. Rules are a relationship to their set and carry no key; they are addressed by their parent plus their resource key. Every demo product has a set, so the demo sells no product that delivers nothing, and the demo ladder's tier picker, which leaves out a product with no set, has products to offer.
|
||||
- Four primary products by `display_category`, keyed `demo_plan` (plan), `demo_addon` (addon), `demo_usage` (usage), `demo_onetime` (onetime). The plan product is linked to `demo_baseline`; `demo_addon`, `demo_usage` and `demo_onetime` are linked to `demo_addons`. (`display_category` is presentation-only grouping metadata, per `product-catalog`; `demo_plan`'s plan-ness is structural — it is a tier on the demo ladder — not a function of this label. The `product_type` column no longer exists after Doc 41.)
|
||||
- Two additional plan-typed products keyed `demo_plan_2` and `demo_plan_3` to populate higher ladder ranks.
|
||||
- One plan ladder keyed `demo_ladder`, named "Demo Ladder", with three tiers at ranks 0, 1, 2, backed by `demo_plan`, `demo_plan_2`, `demo_plan_3` respectively. Tiers are a relationship between a ladder and a product and carry no key.
|
||||
- One issued grant against Alice's `Personal` organization, **product-backed and position-bearing** (Doc 41 §5.1, §9 step 4 rule 2): the grant names `demo_plan` — the lowest-ranked tier product on the demo ladder among the tier products that share the `demo_baseline` entitlement set — and, via the conferral primitive, receives an honest attachment to that ladder at rank 0. A grant is a ledger row and carries no key; it is addressed by its id, resolved through the organization and product the seeder just read back. The grant carries a domain-legal `grant_reason` (`'default'`, with `granted_by_person_id` left NULL, satisfying `default_iff_system_authored`). Seeding a grant that references `demo_baseline` directly is no longer representable: `entitlements.grants.entitlement_set_id` is dropped (Doc 41 §5.1), so every seeded grant must name a product.
|
||||
- Three demo person rows (bob, carlos, diana) with their respective personal organizations and workspaces, seeded via `provisioning.AutoProvision` using synthetic OIDC claims whose subject is the literal UUID the Keycloak seed pins for that user. A person's key is its `oidc_subject`, the external reference the identity provider already assigns, so no `key` column is added and none is written; those organizations and workspaces carry a NULL key, as auto-provisioning always creates them.
|
||||
|
||||
#### Scenario: catalog visible in operator UI
|
||||
|
||||
- **GIVEN** the seeder ran successfully against a freshly-migrated DB and a fresh Keycloak seed
|
||||
- **WHEN** an operator opens `/operator` after logging in as Alice
|
||||
- **THEN** the Products tab shows 6 products, the Plan Ladders tab shows 1 ladder with 3 tiers, the Entitlement Sets tab shows 2 sets with 2 rules each, the Grants tab shows 6 grants (the baseline and the five of the ledger demo), and the People tab shows 4 persons (bob/carlos/diana from seed + Alice from lazy creation on her login)
|
||||
|
||||
#### Scenario: the seeder supplies no identifiers
|
||||
|
||||
- **GIVEN** the seeder is inserting the demo catalog against an empty database
|
||||
- **WHEN** each row is written
|
||||
- **THEN** the insert names the row's `key` and lets the database generate the id
|
||||
- **AND** no id is written as a literal, computed from a label, or otherwise supplied by the seeder
|
||||
|
||||
#### Scenario: baseline grant is product-backed and position-bearing
|
||||
|
||||
- **GIVEN** the seeder is creating the baseline grant for Alice's `Personal` organization
|
||||
- **WHEN** the grant is issued
|
||||
- **THEN** the seeder resolves `demo_plan` by its key, calls the conferral primitive naming that product's id as `p_product_id` and the grant row as the source, and never names `demo_baseline` (or any entitlement set) directly
|
||||
- **AND** the resulting `pool_provisions` row carries `product_id = demo_plan`'s id and the resulting `pool_provision_ladders` row occupies the demo ladder at rank 0, so the grant is visible on the Plan Ladders tab as occupying the lowest tier, not merely on the Entitlement Sets or Grants tabs
|
||||
- **AND** the grant row carries `grant_reason = 'default'` with `granted_by_person_id` NULL, the only domain-legal combination for a system-authored seed grant
|
||||
@@ -57,7 +57,8 @@ Sections 1 to 4 are one commit each, and their items, item 1.0 aside, are that c
|
||||
- the filter without the lifecycle test and the filter without the on-ladder test: caught by T5, `want_new` `tierCandidates(a draft product with a set) =` and `tierCandidates(a product already on the ladder) =`;
|
||||
- `availableTierProducts` not calling `tierCandidates` is not a break of its own: `deadcode` refuses a `tierCandidates` nothing calls;
|
||||
- the seven fixture edits of item 1.1 and the doc comment of item 1.2 carry no break: they remove a state section 2 forbids, and section 2's run is their test.
|
||||
- [x] 1.9 `make screens` (it resets the app database; item 1.0 has run): `operator-plan-ladder-detail`'s `-open` and `-invalid` captures change, at both widths, and no other screen changes because of this section. The demo seed's three products that are tiers of no ladder (`demo-addon`, `demo-usage`, `demo-onetime`, `internal/demoseed/demoseed.go:426-428`) have no entitlement set, so the picker now leaves them out and the demo ladder has no product to add: the open panel shows R4's empty state, and the invalid capture no longer holds an Add tier form to submit. The captures lose the tier add form's filled and invalid states until the demo has an off-ladder product with a set; the demo add-ons with no set are #272's fault in the demo's own data, and its fix gives them sets. The other changed screens are the known data-only differences and the host's time zone (#274).
|
||||
- [x] 1.9 `make screens` (it resets the app database; item 1.0 has run): `operator-plan-ladder-detail`'s `-open` and `-invalid` captures change, at both widths, and no other screen changes because of this section. The demo seed's three products that are tiers of no ladder (`demo-addon`, `demo-usage`, `demo-onetime`, `internal/demoseed/demoseed.go:426-428`) have no entitlement set, so the picker now leaves them out and the demo ladder has no product to add: the open panel shows R4's empty state, and the invalid capture no longer holds an Add tier form to submit. The captures lose the tier add form's filled and invalid states until the demo has an off-ladder product with a set, which item 1.10 gives it; the demo add-ons with no set are #272's fault in the demo's own data. The other changed screens are the known data-only differences and the host's time zone (#274).
|
||||
- [x] 1.10 The demo seed gives its three add-ons a set (`internal/demoseed/demoseed.go`): a second set, `demo_addons`, with the same two rules in the shape of `demo_baseline` (a `limit` of 5 on `demo.widgets`, a `boolean` on `demo.feature-x`), created by key and never written over, and `demo-addon`, `demo-usage` and `demo-onetime` carry it instead of no set. Why: after section 1's commit a capture left the demo ladder with no product to add, the Add tier form was never rendered, and `TestEveryRegisteredFormWasCaptured` failed once `make screens` regenerated `test/screens/out/forms.json`; and an add-on with no set delivers nothing, #272's fault in the demo's own data. The seed's README and `internal/demoseed/testdata/seed/twice.txt` (one more created row) follow.
|
||||
|
||||
## 2. The database refuses a tier whose product has no set
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ subject, and the seeder writes the literal `sub` that
|
||||
| Where | Rows | Notes |
|
||||
|------|------|-------|
|
||||
| `entitlements.resource_keys` | `demo.widgets`, `demo.feature-x` | Catalog vocabulary. Upserted via `ON CONFLICT DO NOTHING`. |
|
||||
| `entitlements.entitlement_sets` | `demo_baseline` (key) / "demo-baseline" (name) | One set, two rules (one `limit` on `demo.widgets`, one `boolean` on `demo.feature-x`). |
|
||||
| `entitlements.entitlement_sets` | `demo_baseline` (key) / "demo-baseline" (name) | One set, two rules (one `limit` on `demo.widgets`, one `boolean` on `demo.feature-x`). The three plan products carry it. |
|
||||
| `entitlements.entitlement_sets` | `demo_addons` (key) / "demo-addons" (name) | One set, the same two rules (a `limit` of 5 on `demo.widgets`, a `boolean` on `demo.feature-x`). The add-on, usage, and one-time products carry it, so each delivers something and the Add tier picker has products to offer. |
|
||||
| `billing.products` | keys `demo_plan`, `demo_plan_2`, `demo_plan_3`, `demo_addon`, `demo_usage`, `demo_onetime` | Six products. Plan products have `product_type = NULL`; their "plan" kind is derived from plan-ladder-tier membership. |
|
||||
| `billing.plan_ladders` | `demo_ladder` (key) / "Demo Ladder" (name) | Three tiers at ranks 0/1/2. |
|
||||
| `identity.users` + `identity.persons` + `organization.organizations` (personal) | bob, carlos, diana | Seeded via `provisioning.AutoProvision`, each carrying as its OIDC subject the literal `sub` `test/seed/keycloak/seed-keycloak.sh` pins for that username. Alice is intentionally NOT pre-seeded — her row is created lazily on her first OIDC login. |
|
||||
@@ -68,7 +69,7 @@ attach the grant.
|
||||
- **People**: 4 rows (bob, carlos, diana from the seeder + alice from lazy creation during this login).
|
||||
- **Products**: 6 rows starting with `demo-`.
|
||||
- **Plan Ladders**: 1 row "Demo Ladder" with 3 tiers.
|
||||
- **Entitlement Sets**: 1 row `demo-baseline` with 2 rules.
|
||||
- **Entitlement Sets**: 2 rows, `demo-baseline` and `demo-addons`, each with 2 rules.
|
||||
- **Grants**: 6 rows the seeder owns after alice has logged in once and seed-demo is re-run: the baseline on her personal org and the five of the ledger demo on Carlos Member's. Each personal org also carries the org-type default grants its pool was floored and restored with.
|
||||
- **Carlos Member's organization**: Tier changes reads, newest first, Started (Default restored after a grant expired), Ended (Grant expired), Upgraded, Started (Default restored after a revocation), Ended (Revoked by an operator), Downgraded, Extended, Upgraded, Upgraded, Started (Seeded default).
|
||||
|
||||
@@ -78,7 +79,7 @@ Or via `psql`:
|
||||
docker exec -it mc-<worktree>-postgres-1 psql -U member_console -d member_console \
|
||||
-c "SELECT (SELECT count(*) FROM core.products WHERE name LIKE 'demo-%') AS products,
|
||||
(SELECT count(*) FROM core.plan_ladders WHERE key = 'demo_ladder') AS ladders,
|
||||
(SELECT count(*) FROM core.entitlement_sets WHERE key = 'demo_baseline') AS sets,
|
||||
(SELECT count(*) FROM core.entitlement_sets WHERE key IN ('demo_baseline', 'demo_addons')) AS sets,
|
||||
(SELECT count(*) FROM core.grants g
|
||||
JOIN core.organizations o ON o.org_id = g.granted_to_org_id
|
||||
JOIN core.persons p ON p.person_id = o.owner_person_id
|
||||
@@ -86,7 +87,7 @@ docker exec -it mc-<worktree>-postgres-1 psql -U member_console -d member_consol
|
||||
OR g.description LIKE 'Ledger demo:%') AS grants;"
|
||||
```
|
||||
|
||||
Expected: `products = 6`, `ladders = 1`, `sets = 1`, `grants = 6` (after Alice has logged in).
|
||||
Expected: `products = 6`, `ladders = 1`, `sets = 2`, `grants = 6` (after Alice has logged in).
|
||||
|
||||
## How to undo
|
||||
|
||||
|
||||
Reference in New Issue
Block a user