Files
member-console/test/mockshot-seed.sql
T
cgalo5758 dd3962990b Adopt entity keys and add invoice numbers
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.
2026-08-29 20:12:04 -05:00

440 lines
22 KiB
PL/PgSQL

-- mockshot-seed.sql — reversible sample data at deployment scale, for
-- screenshots and demos (first used for the README operator-overview shot).
--
-- Identity: the database generates every ID (Decision 30), and this seed
-- addresses the rows it owns by `key` (grammar `^[a-z][a-z0-9_]*$`).
-- Organizations are keyed `mockshot_0001` .. `mockshot_2842` (one per mock
-- member) and `mockshot_t0001` .. `mockshot_t0038` (the teams). Each mock
-- person's user row carries the external reference `mockshot:<org key>`, so a
-- person is found by its OIDC subject and never by its email address, its
-- display name, or a marker column. The catalog rows are keyed
-- `mockshot_hosting` (entitlement set), `mockshot_starter_hosting` and its
-- four siblings (products), and `monthly` / `yearly` within each product
-- (prices). Everything else — pools, workspaces, assignments, accounts,
-- subscriptions, items, invoices, payments, grants, provisions — is reached
-- through its organization or its catalog parent, so it needs no key of its
-- own. mockshot-nuke.sql reverses the seed from exactly those three
-- predicates: the `mockshot_` organization key prefix, the `mockshot:`
-- subject prefix, and the catalog keys.
--
-- Re-running the seed inserts nothing: each keyed insert conflicts on its
-- key, and each keyless insert is guarded by a NOT EXISTS on the parent that
-- already carries the child.
--
-- Run against a test stack (port/creds from bootstrap-stack.sh output):
-- psql -h localhost -p $POSTGRES_PORT -U member_console -d member_console \
-- -v ON_ERROR_STOP=1 -f test/mockshot-seed.sql
-- Reverse with mockshot-nuke.sql the same way. Nuke BEFORE restarting the
-- app: the boot-time entitlement materializer must never reconcile the mock
-- provisions into real pool entitlements.
--
-- Shape follows the app's own mechanics: signup mints a personal org per
-- member (backdated over ~19 months of joins) with its default pool,
-- workspace, and default-plan grant; team orgs are created by long-standing
-- members; upgrading to a paid subscription ends the free default's delivery
-- (ladder exclusion); subscriptions carry priced items so monthly recurring
-- money is real.
--
-- Targets (existing → after): People 5→2,847 (~148 joined in 30 days),
-- Team orgs 0→38, Subscriptions 0→214 (205 active + 9 trialing, MRR
-- $2,160.00), Open invoices 0→9 ($372.00 outstanding), Delivering operator
-- grants →19 with ≈2,669 signup defaults in the caption, Catalog
-- products 10→15.
BEGIN;
-- 2,842 users, joined over ~19 months (i*5h + jitter). The subject carries
-- the organization key the member's personal org gets below, which is how
-- every later statement recovers the member's index without an ID of its own.
INSERT INTO core.users (oidc_subject, created_at)
SELECT 'mockshot:mockshot_' || lpad(i::text, 4, '0'),
now() - (i * 5 || ' hours')::interval - (random() * interval '55 minutes')
FROM generate_series(1, 2842) AS i
ON CONFLICT (oidc_subject) DO NOTHING;
-- The member index, its organization key, and its join date, read back from
-- the rows themselves so a second run sees exactly what the first created.
-- `i` orders members by join date: member 1 is the newest, 2842 the oldest.
CREATE TEMP TABLE mock_members ON COMMIT DROP AS
SELECT split_part(u.oidc_subject, '_', 2)::int AS i,
'mockshot_' || split_part(u.oidc_subject, '_', 2) AS org_key,
u.user_id,
u.created_at AS joined_at
FROM core.users u
WHERE u.oidc_subject LIKE 'mockshot:%';
INSERT INTO core.persons (user_id, display_name, primary_email, primary_email_verified, created_at)
SELECT
m.user_id,
-- Stride 7 is coprime to 20, so consecutive members vary in both names
-- and (first, last) pairs stay unique across lcm(21,20)=420 indices.
(ARRAY['Ada','Basil','Corin','Dara','Emil','Freya','Gus','Hana','Iris','Jonas','Kira',
'Lior','Mara','Nils','Odile','Piotr','Quinn','Rosa','Sana','Theo','Uma'])[1 + (m.i % 21)]
|| ' ' ||
(ARRAY['Aldana','Brook','Calder','Dove','Ellis','Fen','Grange','Hale','Ingram','Jarvis',
'Keel','Lark','Marsh','North','Orr','Penn','Quill','Reyes','Sorrel','Vale'])[1 + ((m.i * 7) % 20)],
-- Realistic data, never a predicate: nothing in this file or in the nuke
-- selects on an address.
'mock-p' || m.i || '@example.test',
true,
m.joined_at + interval '1 minute'
FROM mock_members m
WHERE NOT EXISTS (SELECT 1 FROM core.persons p WHERE p.user_id = m.user_id);
-- One personal org per mock person, keyed with the member's own key and timed
-- the way signup mints them (two minutes after the person row).
INSERT INTO core.organizations (name, key, org_type, owner_person_id, created_at)
SELECT p.display_name || '''s Organization',
m.org_key,
'personal',
p.person_id,
m.joined_at + interval '2 minutes'
FROM mock_members m
JOIN core.persons p ON p.user_id = m.user_id
ON CONFLICT (key) DO NOTHING;
-- The team org type (design taxonomy: personal / team / enterprise —
-- design/organization/architecture.md §2). Org types are vocabulary whose
-- primary key is the type itself, so there is nothing to key; the nuke
-- removes it only if no organization is left using it.
INSERT INTO core.org_types (org_type, display_name, description, is_active)
VALUES ('team', 'Team', 'An organization more than one person works in.', true)
ON CONFLICT (org_type) DO NOTHING;
-- 38 team orgs created by long-standing members (the oldest signups), each
-- 10-70 days after its owner joined.
INSERT INTO core.organizations (name, key, org_type, owner_person_id, created_at)
SELECT
CASE WHEN k <= 4 THEN
(ARRAY['Cedar Print Collective','Prairie Signal Cooperative',
'Harbor Works Guild','Juniper Commons'])[k]
ELSE
(ARRAY['Cedar','Prairie','Harbor','Juniper','Granite','Meadow','Larkspur','Alder'])[1 + ((k - 5) % 8)]
|| ' ' ||
(ARRAY['Collective','Cooperative','Commons','Works','Guild','Studio','Assembly','Exchange'])[1 + ((k - 5) / 8)]
END,
'mockshot_t' || lpad(k::text, 4, '0'),
'team',
p.person_id,
m.joined_at + interval '10 days' + (random() * interval '60 days')
FROM generate_series(1, 38) AS k
JOIN mock_members m ON m.i = 2843 - k
JOIN core.persons p ON p.user_id = m.user_id
ON CONFLICT (key) DO NOTHING;
-- Every org gets the default pool and workspace signup mints for it
-- (provisioning.CreateWorkspaceWithPrimaryAssignment), so mock provisions
-- land in the org's own pool instead of borrowing somebody else's. The pool
-- is the one row the system keys by design, `default`; the workspace carries
-- no key, and the assignment is the pair, so neither does it.
INSERT INTO core.resource_pools (org_id, name, key, pool_type, is_auto_managed, created_at)
SELECT o.org_id, 'Default', 'default', 'default', TRUE, o.created_at
FROM core.organizations o
WHERE o.key LIKE 'mockshot\_%'
ON CONFLICT (org_id, key) DO NOTHING;
INSERT INTO core.workspaces (org_id, name, created_at)
SELECT o.org_id, 'Default', o.created_at
FROM core.organizations o
WHERE o.key LIKE 'mockshot\_%'
AND NOT EXISTS (SELECT 1 FROM core.workspaces w WHERE w.org_id = o.org_id);
INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, created_at)
SELECT rp.pool_id, w.workspace_id, TRUE, o.created_at
FROM core.organizations o
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
JOIN core.workspaces w ON w.org_id = o.org_id
WHERE o.key LIKE 'mockshot\_%'
AND NOT EXISTS (
SELECT 1 FROM core.pool_assignments pa
WHERE pa.pool_id = rp.pool_id AND pa.workspace_id = w.workspace_id
);
-- Billing accounts: every team + the 176 most recent personal orgs.
INSERT INTO core.accounts (org_id, name, created_at)
SELECT o.org_id, o.name || ' billing', o.created_at
FROM (
SELECT org_id, name, created_at
FROM core.organizations
WHERE key LIKE 'mockshot\_%'
ORDER BY (key LIKE 'mockshot\_t%') DESC, created_at DESC
LIMIT 214
) o
WHERE NOT EXISTS (SELECT 1 FROM core.accounts a WHERE a.org_id = o.org_id);
-- 214 live subscriptions: teams first (all active), then individuals;
-- the last 9 are trialing.
INSERT INTO core.subscriptions (billing_account_id, status, current_period_start, current_period_end)
SELECT billing_account_id,
CASE WHEN rn <= 205 THEN 'active' ELSE 'trialing' END,
date_trunc('month', now()),
date_trunc('month', now()) + interval '1 month'
FROM (
SELECT a.billing_account_id,
row_number() OVER (ORDER BY (o.key LIKE 'mockshot\_t%') DESC, a.billing_account_id) AS rn
FROM core.accounts a
JOIN core.organizations o ON o.org_id = a.org_id
WHERE o.key LIKE 'mockshot\_%'
AND NOT EXISTS (SELECT 1 FROM core.subscriptions s WHERE s.billing_account_id = a.billing_account_id)
) accts;
-- The entitlement the mock plans confer. One set, one numeric rule, so the
-- mock provisions carry a real entitlement set instead of borrowing one.
INSERT INTO core.entitlement_sets (name, key, description, is_active)
VALUES ('Hosting baseline', 'mockshot_hosting',
'Entitlements every hosting plan confers.', TRUE)
ON CONFLICT (key) DO NOTHING;
INSERT INTO core.entitlement_set_rules (set_id, rule_type, resource_key,
resource_value, resource_per_unit, stacking_policy, description)
SELECT es.set_id, 'limit', 'fedwiki_sites', 5, FALSE, 'additive', 'Up to 5 sites.'
FROM core.entitlement_sets es
WHERE es.key = 'mockshot_hosting'
AND NOT EXISTS (
SELECT 1 FROM core.entitlement_set_rules r
WHERE r.set_id = es.set_id AND r.resource_key = 'fedwiki_sites'
);
-- Five mock products with deployment-plausible names (before the prices,
-- items, and grants that reference them). Published so grant events print
-- them.
INSERT INTO core.products (name, key, description, display_category,
entitlement_set_id, lifecycle_status)
SELECT v.name, v.key, v.description, v.display_category,
CASE WHEN v.confers THEN es.set_id END,
'published'
FROM (VALUES
('Starter Hosting', 'mockshot_starter_hosting', 'Default plan for new members', NULL, TRUE),
('Standard Hosting', 'mockshot_standard_hosting', 'Paid hosting tier', NULL, TRUE),
('Sustainer Hosting', 'mockshot_sustainer_hosting', 'Supporter hosting tier', NULL, TRUE),
('Custom Domain', 'mockshot_custom_domain', 'Serve a site on your own domain', 'addon', FALSE),
('Storage Boost', 'mockshot_storage_boost', 'Additional media storage', 'addon', FALSE)
) AS v(name, key, description, display_category, confers)
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
ON CONFLICT (key) DO NOTHING;
-- Prices for the paid tiers, keyed within their product: Standard $9/mo
-- (default), Sustainer $18/mo (default) with an $180/yr alternative — the
-- yearly price exercises the one-twelfth normalization on the overview's
-- money headline.
INSERT INTO core.prices (product_id, key, currency, unit_amount, recurring_interval, is_default)
SELECT p.product_id, v.key, 'usd', v.unit_amount, v.recurring_interval, v.is_default
FROM (VALUES
('mockshot_standard_hosting', 'monthly', 900, 'month', TRUE),
('mockshot_sustainer_hosting', 'monthly', 1800, 'month', TRUE),
('mockshot_sustainer_hosting', 'yearly', 18000, 'year', FALSE)
) AS v(product_key, key, unit_amount, recurring_interval, is_default)
JOIN core.products p ON p.key = v.product_key
ON CONFLICT (product_id, key) DO NOTHING;
-- Subscription items: teams ride Sustainer (every 4th on the yearly price),
-- individuals ride Standard monthly. The team's number comes out of its own
-- key, the only arithmetic the rest of the file needs.
INSERT INTO core.subscription_items (subscription_id, product_id, price_id, quantity)
SELECT s.subscription_id, pr.product_id, pr.price_id, 1
FROM core.subscriptions s
JOIN core.accounts a ON a.billing_account_id = s.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id
JOIN LATERAL (
SELECT p.price_id, p.product_id
FROM core.prices p
JOIN core.products pd ON pd.product_id = p.product_id
WHERE CASE
WHEN o.key LIKE 'mockshot\_t%' THEN
pd.key = 'mockshot_sustainer_hosting'
AND p.key = CASE WHEN split_part(o.key, '_t', 2)::int % 4 = 0 THEN 'yearly' ELSE 'monthly' END
ELSE pd.key = 'mockshot_standard_hosting' AND p.key = 'monthly'
END
LIMIT 1
) pr ON true
WHERE o.key LIKE 'mockshot\_%'
AND NOT EXISTS (
SELECT 1 FROM core.subscription_items si WHERE si.subscription_id = s.subscription_id
);
-- The signup default-plan grant for every mock org, stamped minutes after
-- the org itself (system-authored: reason 'default' ⟺ no granting person,
-- per chk_grants_default_iff_system_authored).
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id,
grant_reason, description, valid_from, created_at)
SELECT pd.product_id, o.org_id, NULL, 'default', 'Plan issued at signup',
o.created_at + interval '2 minutes',
o.created_at + interval '2 minutes'
FROM core.organizations o
CROSS JOIN (SELECT product_id FROM core.products WHERE key = 'mockshot_starter_hosting') pd
WHERE o.key LIKE 'mockshot\_%'
AND NOT EXISTS (
SELECT 1 FROM core.grants g
WHERE g.granted_to_org_id = o.org_id AND g.grant_reason = 'default'
);
-- Each default grant delivers in its own org's pool.
INSERT INTO core.pool_provisions (pool_id, grant_id, status, entitlement_set_id, product_id, activated_at)
SELECT rp.pool_id, g.grant_id, 'active', es.set_id, g.product_id, g.created_at
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE g.grant_reason = 'default'
AND NOT EXISTS (SELECT 1 FROM core.pool_provisions pp WHERE pp.grant_id = g.grant_id);
-- Auto-provisioning: every live mock subscription delivers its plan, so a
-- paying org is never provision-less after its free default ends below.
INSERT INTO core.pool_provisions (pool_id, subscription_id, status, entitlement_set_id, product_id)
SELECT rp.pool_id, s.subscription_id, 'active', es.set_id, si.product_id
FROM core.subscriptions s
JOIN core.accounts a ON a.billing_account_id = s.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
JOIN core.subscription_items si ON si.subscription_id = s.subscription_id
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE NOT EXISTS (
SELECT 1 FROM core.pool_provisions pp
WHERE pp.subscription_id = s.subscription_id AND pp.product_id = si.product_id
);
-- Eighteen operator-issued extras (upgrades, promos) over the last ~3 days,
-- on the 18 newest members' orgs. The operator is a real console person,
-- never one of the mock members: mock members are exactly the users whose
-- subject carries the mockshot prefix.
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id,
grant_reason, description, valid_from, created_at)
SELECT pd.product_id,
o.org_id,
op.person_id,
CASE WHEN m.i % 2 = 0 THEN 'manual' ELSE 'promotional' END,
'Issued by an operator',
now() - (m.i * interval '4 hours') - interval '20 minutes',
now() - (m.i * interval '4 hours') - interval '20 minutes'
FROM mock_members m
JOIN core.organizations o ON o.key = m.org_key
JOIN core.products pd ON pd.key = CASE
WHEN m.i % 4 = 1 THEN 'mockshot_standard_hosting'
WHEN m.i % 4 = 2 THEN 'mockshot_sustainer_hosting'
WHEN m.i % 4 = 3 THEN 'mockshot_custom_domain'
ELSE 'mockshot_storage_boost' END
CROSS JOIN LATERAL (
SELECT p.person_id
FROM core.persons p
JOIN core.users u ON u.user_id = p.user_id
WHERE u.oidc_subject NOT LIKE 'mockshot:%'
ORDER BY (p.display_name ILIKE 'alice%') DESC, p.created_at
LIMIT 1
) op
WHERE m.i <= 18
AND NOT EXISTS (
SELECT 1 FROM core.grants g
WHERE g.granted_to_org_id = o.org_id AND g.grant_reason <> 'default'
);
INSERT INTO core.pool_provisions (pool_id, grant_id, status, entitlement_set_id, product_id, activated_at)
SELECT rp.pool_id, g.grant_id, 'active', es.set_id, g.product_id, g.created_at
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE g.grant_reason <> 'default'
AND NOT EXISTS (SELECT 1 FROM core.pool_provisions pp WHERE pp.grant_id = g.grant_id);
-- Invoices, one per billing account across the first fifteen teams
-- (invoice-numbers change, data model v16.2): the number is the platform's
-- own, per billing account, a zero-padded four-digit decimal starting at
-- 0001 — and since every account here holds exactly one seeded invoice,
-- every one of the fifteen is numbered 0001. Each account's
-- next_invoice_number moves to 2 below, past the number this seed assigned.
--
-- 9 open invoices, on teams t0001..t0009, staggered over the last two days
-- for the timeline.
INSERT INTO core.invoices (billing_account_id, status, amount_due, amount_paid, currency,
period_start, period_end, due_date, created_at, invoice_number)
SELECT a.billing_account_id, 'open',
(ARRAY[2400, 4800, 1200, 9600, 2400, 7200, 4800, 1200, 3600])[n], 0, 'usd',
now() - interval '1 month', now(), now() + interval '14 days',
now() - (n * interval '5 hours'),
'0001'
FROM generate_series(1, 9) AS n
JOIN core.organizations o ON o.key = 'mockshot_t' || lpad(n::text, 4, '0')
JOIN core.accounts a ON a.org_id = o.org_id
WHERE NOT EXISTS (
SELECT 1 FROM core.invoices i WHERE i.billing_account_id = a.billing_account_id
);
-- 6 recently paid invoices, on teams t0010..t0015, each settled by a
-- succeeded payment two minutes after issue — the timeline's
-- payment_received rows.
INSERT INTO core.invoices (billing_account_id, status, amount_due, amount_paid, currency,
period_start, period_end, due_date, paid_at, created_at, invoice_number)
SELECT a.billing_account_id, 'paid',
(ARRAY[2400, 4800, 2400, 9600, 4800, 2400])[n],
(ARRAY[2400, 4800, 2400, 9600, 4800, 2400])[n], 'usd',
now() - interval '1 month', now(), now() + interval '14 days',
now() - (n * interval '11 hours') + interval '2 minutes',
now() - (n * interval '11 hours'),
'0001'
FROM generate_series(1, 6) AS n
JOIN core.organizations o ON o.key = 'mockshot_t' || lpad((n + 9)::text, 4, '0')
JOIN core.accounts a ON a.org_id = o.org_id
WHERE NOT EXISTS (
SELECT 1 FROM core.invoices i WHERE i.billing_account_id = a.billing_account_id
);
INSERT INTO core.payments (invoice_id, billing_account_id, amount, currency, status, created_at)
SELECT i.invoice_id, i.billing_account_id, i.amount_paid, 'usd', 'succeeded',
i.created_at + interval '2 minutes'
FROM core.invoices i
JOIN core.accounts a ON a.billing_account_id = i.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id AND o.key LIKE 'mockshot\_t%'
WHERE i.status = 'paid'
AND NOT EXISTS (SELECT 1 FROM core.payments pm WHERE pm.invoice_id = i.invoice_id);
-- Each of the fifteen accounts just numbered one invoice 0001; its counter
-- moves to 2. The `<> 2` guard makes a second run of this seed a no-op here
-- as well as everywhere above.
UPDATE core.accounts a
SET next_invoice_number = 2
FROM core.organizations o
WHERE o.org_id = a.org_id
AND o.key LIKE 'mockshot\_t%'
AND split_part(o.key, '_t', 2)::int <= 15
AND a.next_invoice_number <> 2;
-- Upgrading ends the free default's delivery: the ladder table's exclusion
-- admits one active provision per pool per ladder, so each subscribed org's
-- default-grant provision ended when its paid subscription provisioned. The
-- grant itself stays 'active' in the ledger — exactly the ledger-vs-delivery
-- distinction the overview tile is built around.
UPDATE core.pool_provisions pp
SET status = 'ended',
ended_at = pp.activated_at + interval '40 minutes'
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id
JOIN core.accounts a ON a.org_id = g.granted_to_org_id
JOIN core.subscriptions s ON s.billing_account_id = a.billing_account_id
WHERE pp.grant_id = g.grant_id
AND g.grant_reason = 'default'
AND o.key LIKE 'mockshot\_%'
AND pp.status <> 'ended';
COMMIT;
-- Post-seed tallies
SELECT (SELECT count(*) FROM core.persons WHERE status = 'active') AS people,
(SELECT count(*) FROM core.persons WHERE status = 'active'
AND created_at > now() - interval '30 days') AS joined_30d,
(SELECT count(*) FROM core.organizations o JOIN core.org_types t ON t.org_type = o.org_type
WHERE o.status = 'active' AND o.org_type <> 'personal' AND NOT t.is_reserved) AS team_orgs,
(SELECT count(*) FROM core.subscriptions WHERE status = 'active') AS active_subs,
(SELECT count(*) FROM core.subscriptions WHERE status = 'trialing') AS trialing_subs,
(SELECT sum(CASE p.recurring_interval WHEN 'month' THEN p.unit_amount * si.quantity
WHEN 'year' THEN (p.unit_amount * si.quantity) / 12 ELSE 0 END)
FROM core.subscriptions s
JOIN core.subscription_items si ON si.subscription_id = s.subscription_id
JOIN core.prices p ON p.price_id = si.price_id
WHERE s.status = 'active') AS mrr_cents,
(SELECT count(*) FROM core.invoices WHERE status = 'open') AS open_invoices,
(SELECT count(DISTINCT g.grant_id) FROM core.grants g
JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
WHERE pp.status = 'active' AND g.grant_reason <> 'default') AS operator_grants,
(SELECT count(DISTINCT g.grant_id) FROM core.grants g
JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
WHERE pp.status = 'active' AND g.grant_reason = 'default') AS default_grants;