Guard cluster-global CREATE ROLE in all five migration streams with
pg_roles checks so multiple databases can migrate in one cluster, and
tolerate still-referenced roles on Down.
Add test/reset-test-db.sh to drop and recreate member_console_test and
member_console_e2e per run, emit their DSNs from bootstrap, and add a
make test target that resets then runs the suite serialized; parallel
unit packages sharing one database still interfered even after the e2e
split.
Fix customdomain_db_test.go, stale since 0affda7 and previously passing
only through pollution. Bootstrap and the Makefile carry small forward
references to the compose-profile knob introduced next.
Archives the test-db-isolation change.
1254 lines
55 KiB
PL/PgSQL
1254 lines
55 KiB
PL/PgSQL
-- +goose Up
|
|
|
|
-- Core baseline: schema-consolidation change (see openspec/changes/
|
|
-- schema-consolidation). Collapses the five former per-module schemas
|
|
-- (identity, organization, billing, entitlements, integration) into a single
|
|
-- `core` schema -- see design.md Decision 1/2 for why this is a fresh
|
|
-- baseline rather than an ALTER ... SET SCHEMA transition path, and
|
|
-- proposal.md for the rationale (no 1:1 module<->model correspondence).
|
|
-- Authored clean-slate against the structural reference derived from the
|
|
-- pre-consolidation migration chain (git history at bf9a38f); not a
|
|
-- transcription of the old file layout.
|
|
--
|
|
-- `stripe` and `fedwiki` are unchanged, integration-owned schemas with their
|
|
-- own migration streams (internal/stripe/migrations, internal/fedwiki/
|
|
-- migrations) -- not part of this file.
|
|
|
|
CREATE SCHEMA core;
|
|
|
|
-- Required for the GiST exclusion constraint on core.pool_provision_ladders
|
|
-- (equality comparison on UUID/VARCHAR columns alongside the range overlap).
|
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
|
|
|
-- Shared trigger function for BEFORE UPDATE ... SET updated_at = NOW().
|
|
-- Stays in `public` (shared infrastructure, not part of the schema rename
|
|
-- map): used by nearly every table below and by the stripe/fedwiki streams.
|
|
-- +goose StatementBegin
|
|
CREATE FUNCTION public.update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
-- Entitlements-domain trigger functions. Defined here (ahead of the tables
|
|
-- that use them) since their bodies reference core.pool_provision_ladders /
|
|
-- core.grants, which do not exist yet at this point in the file -- safe
|
|
-- because CREATE FUNCTION only parses the body, it does not resolve table
|
|
-- references until the function is invoked.
|
|
|
|
-- Propagates pool_id/status/activated_at/ended_at from a pool_provisions row
|
|
-- to every pool_provision_ladders row attached to it, so the ladder table's
|
|
-- GiST exclusion predicate always reflects the provision's current state.
|
|
-- +goose StatementBegin
|
|
CREATE FUNCTION core.sync_pool_provision_ladders()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.pool_id IS DISTINCT FROM OLD.pool_id
|
|
OR NEW.status IS DISTINCT FROM OLD.status
|
|
OR NEW.activated_at IS DISTINCT FROM OLD.activated_at
|
|
OR NEW.ended_at IS DISTINCT FROM OLD.ended_at THEN
|
|
UPDATE core.pool_provision_ladders
|
|
SET pool_id = NEW.pool_id,
|
|
status = NEW.status,
|
|
activated_at = NEW.activated_at,
|
|
ended_at = NEW.ended_at
|
|
WHERE provision_id = NEW.provision_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
-- Grant lineage same-org enforcement: a grant with extends_grant_id set must
|
|
-- share its parent grant's granted_to_org_id, and both must be non-NULL.
|
|
-- Restricted to org-scoped grants; billing-account/person grant lineage is
|
|
-- out of scope (entitlements/00010 in the old chain).
|
|
-- +goose StatementBegin
|
|
CREATE FUNCTION core.check_grant_lineage_same_org()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
parent_org_id UUID;
|
|
BEGIN
|
|
IF NEW.extends_grant_id IS NULL THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
-- Self-reference is rejected by chk_grants_no_self_extend; defer to it
|
|
-- so the CHECK-violation path fires instead of this trigger (BEFORE
|
|
-- triggers run before CHECK constraints).
|
|
IF NEW.extends_grant_id = NEW.grant_id THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF NEW.granted_to_org_id IS NULL THEN
|
|
RAISE EXCEPTION 'extends_grant_id requires granted_to_org_id to be non-NULL'
|
|
USING ERRCODE = 'check_violation';
|
|
END IF;
|
|
|
|
SELECT granted_to_org_id INTO parent_org_id
|
|
FROM core.grants
|
|
WHERE grant_id = NEW.extends_grant_id;
|
|
|
|
IF parent_org_id IS NULL OR parent_org_id != NEW.granted_to_org_id THEN
|
|
RAISE EXCEPTION 'extends_grant_id must reference a grant for the same organization'
|
|
USING ERRCODE = 'check_violation';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
-- Grant lineage immutability: extends_grant_id is fixed at INSERT and may
|
|
-- never change, preventing post-hoc cycle creation.
|
|
-- +goose StatementBegin
|
|
CREATE FUNCTION core.check_grant_lineage_immutable()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF OLD.extends_grant_id IS DISTINCT FROM NEW.extends_grant_id THEN
|
|
RAISE EXCEPTION 'extends_grant_id is immutable after insert'
|
|
USING ERRCODE = 'check_violation';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Identity
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE core.users (
|
|
user_id UUID NOT NULL DEFAULT uuidv7(),
|
|
oidc_subject TEXT NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
last_login_at TIMESTAMPTZ,
|
|
last_login_ip TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_users PRIMARY KEY (user_id),
|
|
CONSTRAINT uq_users_oidc_subject UNIQUE (oidc_subject)
|
|
);
|
|
|
|
CREATE TRIGGER trigger_users_updated_at
|
|
BEFORE UPDATE ON core.users
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- Redundant with uq_users_oidc_subject; the old chain carried both,
|
|
-- preserved as-is (candidate for a future documented drop).
|
|
CREATE INDEX idx_users_oidc_subject ON core.users(oidc_subject);
|
|
|
|
CREATE TABLE core.persons (
|
|
person_id UUID NOT NULL DEFAULT uuidv7(),
|
|
user_id UUID NOT NULL,
|
|
display_name VARCHAR(255) NOT NULL,
|
|
primary_email VARCHAR(255) NOT NULL,
|
|
primary_email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_persons PRIMARY KEY (person_id),
|
|
CONSTRAINT uq_persons_user_id UNIQUE (user_id),
|
|
CONSTRAINT fk_persons_user_id FOREIGN KEY (user_id) REFERENCES core.users(user_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TRIGGER trigger_persons_updated_at
|
|
BEFORE UPDATE ON core.persons
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- Redundant with uq_persons_user_id; the old chain carried both,
|
|
-- preserved as-is (candidate for a future documented drop).
|
|
CREATE INDEX idx_persons_user_id ON core.persons(user_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Catalog: products, prices, plan ladders
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- entitlement_set_id has NO FK: entitlements.set_id FK was never actually
|
|
-- added anywhere in the old migration chain despite a comment claiming
|
|
-- otherwise (billing/00003) -- a genuine pre-existing gap, preserved as-is
|
|
-- (see task summary blockers).
|
|
CREATE TABLE core.products (
|
|
product_id UUID NOT NULL DEFAULT uuidv7(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
product_type VARCHAR(50),
|
|
metadata JSONB,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
entitlement_set_id UUID,
|
|
lifecycle_status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
|
CONSTRAINT pk_products PRIMARY KEY (product_id),
|
|
CONSTRAINT chk_products_product_type_domain CHECK (product_type IS NULL OR product_type IN ('addon', 'usage', 'one_time')),
|
|
CONSTRAINT chk_products_lifecycle_status_domain CHECK (lifecycle_status IN ('draft', 'published', 'retired'))
|
|
);
|
|
|
|
CREATE INDEX idx_products_is_active ON core.products(is_active);
|
|
CREATE INDEX idx_products_is_public ON core.products(is_public);
|
|
|
|
CREATE TRIGGER trigger_products_updated_at
|
|
BEFORE UPDATE ON core.products
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.prices (
|
|
price_id UUID NOT NULL DEFAULT uuidv7(),
|
|
product_id UUID NOT NULL,
|
|
currency VARCHAR(3) NOT NULL,
|
|
unit_amount INTEGER NOT NULL,
|
|
recurring_interval VARCHAR(10),
|
|
trial_period_days INTEGER,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
CONSTRAINT pk_prices PRIMARY KEY (price_id),
|
|
CONSTRAINT fk_prices_product_id FOREIGN KEY (product_id) REFERENCES core.products(product_id)
|
|
);
|
|
|
|
CREATE INDEX idx_prices_product_id ON core.prices(product_id);
|
|
CREATE INDEX idx_prices_is_active ON core.prices(is_active);
|
|
CREATE UNIQUE INDEX uq_prices_one_default_per_product ON core.prices(product_id) WHERE is_default;
|
|
|
|
CREATE TRIGGER trigger_prices_updated_at
|
|
BEFORE UPDATE ON core.prices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.plan_ladders (
|
|
plan_ladder_id UUID NOT NULL DEFAULT uuidv7(),
|
|
ladder_key VARCHAR(100) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
CONSTRAINT pk_plan_ladders PRIMARY KEY (plan_ladder_id),
|
|
CONSTRAINT uq_plan_ladders_ladder_key UNIQUE (ladder_key)
|
|
);
|
|
|
|
CREATE TRIGGER trigger_plan_ladders_updated_at
|
|
BEFORE UPDATE ON core.plan_ladders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Organizations & access control
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- 'system' org_type is never seeded by any migration (created by
|
|
-- app/bootstrap code); is_reserved backfill for it is therefore a
|
|
-- historical no-op, not reproduced.
|
|
CREATE TABLE core.org_types (
|
|
org_type VARCHAR(20) NOT NULL,
|
|
display_name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
is_reserved BOOLEAN NOT NULL DEFAULT FALSE,
|
|
default_plan_ladder_id UUID,
|
|
CONSTRAINT pk_org_types PRIMARY KEY (org_type),
|
|
CONSTRAINT fk_org_types_default_plan_ladder FOREIGN KEY (default_plan_ladder_id) REFERENCES core.plan_ladders(plan_ladder_id)
|
|
);
|
|
|
|
CREATE TRIGGER trigger_org_types_updated_at
|
|
BEFORE UPDATE ON core.org_types
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.organizations (
|
|
org_id UUID NOT NULL DEFAULT uuidv7(),
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(100) NOT NULL,
|
|
org_type VARCHAR(20) NOT NULL,
|
|
owner_person_id UUID NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_organizations PRIMARY KEY (org_id),
|
|
CONSTRAINT uq_organizations_slug UNIQUE (slug),
|
|
CONSTRAINT fk_organizations_owner_person_id FOREIGN KEY (owner_person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_organizations_org_type FOREIGN KEY (org_type) REFERENCES core.org_types(org_type)
|
|
);
|
|
|
|
CREATE INDEX idx_organizations_owner ON core.organizations(owner_person_id);
|
|
CREATE INDEX idx_organizations_slug ON core.organizations(slug);
|
|
|
|
CREATE TRIGGER trigger_organizations_updated_at
|
|
BEFORE UPDATE ON core.organizations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- org_id NULL = system-wide role (seeded below:
|
|
-- owner/admin/member/billing/viewer/platform_admin).
|
|
CREATE TABLE core.roles (
|
|
role_id UUID NOT NULL DEFAULT uuidv7(),
|
|
org_id UUID,
|
|
role_name VARCHAR(100) NOT NULL,
|
|
display_name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
|
permissions TEXT[] NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_roles PRIMARY KEY (role_id),
|
|
CONSTRAINT fk_roles_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id)
|
|
);
|
|
|
|
CREATE INDEX idx_roles_org_id ON core.roles(org_id);
|
|
|
|
CREATE TRIGGER trigger_roles_updated_at
|
|
BEFORE UPDATE ON core.roles
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.org_members (
|
|
org_member_id UUID NOT NULL DEFAULT uuidv7(),
|
|
org_id UUID NOT NULL,
|
|
person_id UUID NOT NULL,
|
|
role_id UUID NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
removed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_org_members PRIMARY KEY (org_member_id),
|
|
CONSTRAINT uq_org_members_org_id_person_id UNIQUE (org_id, person_id),
|
|
CONSTRAINT fk_org_members_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id),
|
|
CONSTRAINT fk_org_members_person_id FOREIGN KEY (person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_org_members_role_id FOREIGN KEY (role_id) REFERENCES core.roles(role_id)
|
|
);
|
|
|
|
CREATE INDEX idx_org_members_org_id ON core.org_members(org_id);
|
|
CREATE INDEX idx_org_members_person_id ON core.org_members(person_id);
|
|
|
|
CREATE TRIGGER trigger_org_members_updated_at
|
|
BEFORE UPDATE ON core.org_members
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.workspaces (
|
|
workspace_id UUID NOT NULL DEFAULT uuidv7(),
|
|
org_id UUID NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_workspaces PRIMARY KEY (workspace_id),
|
|
CONSTRAINT uq_workspaces_org_id_slug UNIQUE (org_id, slug),
|
|
CONSTRAINT fk_workspaces_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id)
|
|
);
|
|
|
|
CREATE INDEX idx_workspaces_org_id ON core.workspaces(org_id);
|
|
|
|
CREATE TRIGGER trigger_workspaces_updated_at
|
|
BEFORE UPDATE ON core.workspaces
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.role_assignments (
|
|
assignment_id UUID NOT NULL DEFAULT uuidv7(),
|
|
role_id UUID NOT NULL,
|
|
person_id UUID NOT NULL,
|
|
org_id UUID NOT NULL,
|
|
scope_type VARCHAR(20) NOT NULL,
|
|
scope_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_role_assignments PRIMARY KEY (assignment_id),
|
|
CONSTRAINT uq_role_assignments_scope UNIQUE (role_id, person_id, org_id, scope_type, scope_id),
|
|
CONSTRAINT fk_role_assignments_role_id FOREIGN KEY (role_id) REFERENCES core.roles(role_id),
|
|
CONSTRAINT fk_role_assignments_person_id FOREIGN KEY (person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_role_assignments_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id)
|
|
);
|
|
|
|
CREATE INDEX idx_role_assignments_person_id ON core.role_assignments(person_id);
|
|
CREATE INDEX idx_role_assignments_org_id ON core.role_assignments(org_id);
|
|
|
|
CREATE TRIGGER trigger_role_assignments_updated_at
|
|
BEFORE UPDATE ON core.role_assignments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Billing: accounts, subscriptions, invoicing & payments
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE core.accounts (
|
|
billing_account_id UUID NOT NULL DEFAULT uuidv7(),
|
|
org_id UUID NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_accounts PRIMARY KEY (billing_account_id),
|
|
CONSTRAINT fk_accounts_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id)
|
|
);
|
|
|
|
CREATE INDEX idx_accounts_org_id ON core.accounts(org_id);
|
|
|
|
CREATE TRIGGER trigger_accounts_updated_at
|
|
BEFORE UPDATE ON core.accounts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.subscriptions (
|
|
subscription_id UUID NOT NULL DEFAULT uuidv7(),
|
|
billing_account_id UUID NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
current_period_start TIMESTAMPTZ,
|
|
current_period_end TIMESTAMPTZ,
|
|
cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE,
|
|
canceled_at TIMESTAMPTZ,
|
|
ended_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
commitment_end TIMESTAMPTZ,
|
|
commitment_renewal VARCHAR(20),
|
|
early_termination_policy VARCHAR(20),
|
|
CONSTRAINT pk_subscriptions PRIMARY KEY (subscription_id),
|
|
CONSTRAINT fk_subscriptions_billing_account_id FOREIGN KEY (billing_account_id) REFERENCES core.accounts(billing_account_id),
|
|
CONSTRAINT chk_subscriptions_commitment_renewal CHECK (commitment_renewal IS NULL OR commitment_renewal IN ('auto_renew', 'expire')),
|
|
CONSTRAINT chk_subscriptions_early_termination_policy CHECK (early_termination_policy IS NULL OR early_termination_policy IN ('block', 'fee', 'allow')),
|
|
CONSTRAINT chk_subscriptions_commitment_fields_coherent CHECK ((commitment_end IS NULL AND early_termination_policy IS NULL)
|
|
OR (commitment_end IS NOT NULL AND early_termination_policy IS NOT NULL))
|
|
);
|
|
|
|
CREATE INDEX idx_subscriptions_billing_account_id ON core.subscriptions(billing_account_id);
|
|
CREATE INDEX idx_subscriptions_status ON core.subscriptions(status);
|
|
|
|
CREATE TRIGGER trigger_subscriptions_updated_at
|
|
BEFORE UPDATE ON core.subscriptions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.subscription_items (
|
|
subscription_item_id UUID NOT NULL DEFAULT uuidv7(),
|
|
subscription_id UUID NOT NULL,
|
|
product_id UUID NOT NULL,
|
|
price_id UUID NOT NULL,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_subscription_items PRIMARY KEY (subscription_item_id),
|
|
CONSTRAINT fk_subscription_items_subscription_id FOREIGN KEY (subscription_id) REFERENCES core.subscriptions(subscription_id),
|
|
CONSTRAINT fk_subscription_items_product_id FOREIGN KEY (product_id) REFERENCES core.products(product_id),
|
|
CONSTRAINT fk_subscription_items_price_id FOREIGN KEY (price_id) REFERENCES core.prices(price_id)
|
|
);
|
|
|
|
CREATE INDEX idx_subscription_items_subscription_id ON core.subscription_items(subscription_id);
|
|
|
|
CREATE TRIGGER trigger_subscription_items_updated_at
|
|
BEFORE UPDATE ON core.subscription_items
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- append-only log; no updated_at column, correctly no trigger.
|
|
CREATE TABLE core.subscription_changes (
|
|
change_id UUID NOT NULL DEFAULT uuidv7(),
|
|
subscription_id UUID NOT NULL,
|
|
previous_status TEXT,
|
|
new_status TEXT NOT NULL,
|
|
stripe_event_id TEXT NOT NULL,
|
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_subscription_changes PRIMARY KEY (change_id),
|
|
CONSTRAINT fk_subscription_changes_subscription_id FOREIGN KEY (subscription_id) REFERENCES core.subscriptions(subscription_id)
|
|
);
|
|
|
|
CREATE INDEX idx_subscription_changes_subscription_id ON core.subscription_changes(subscription_id);
|
|
|
|
CREATE TABLE core.payment_methods (
|
|
payment_method_id UUID NOT NULL DEFAULT uuidv7(),
|
|
billing_account_id UUID NOT NULL,
|
|
pm_type TEXT NOT NULL,
|
|
card_brand TEXT,
|
|
card_last4 TEXT,
|
|
card_exp_month INTEGER,
|
|
card_exp_year INTEGER,
|
|
funding TEXT,
|
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
detached_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_payment_methods PRIMARY KEY (payment_method_id),
|
|
CONSTRAINT fk_payment_methods_billing_account_id FOREIGN KEY (billing_account_id) REFERENCES core.accounts(billing_account_id)
|
|
);
|
|
|
|
CREATE INDEX idx_payment_methods_billing_account_id ON core.payment_methods(billing_account_id);
|
|
|
|
CREATE TRIGGER trigger_payment_methods_updated_at
|
|
BEFORE UPDATE ON core.payment_methods
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.invoices (
|
|
invoice_id UUID NOT NULL DEFAULT uuidv7(),
|
|
billing_account_id UUID NOT NULL,
|
|
subscription_id UUID,
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
amount_due INTEGER NOT NULL DEFAULT 0,
|
|
amount_paid INTEGER NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL,
|
|
period_start TIMESTAMPTZ,
|
|
period_end TIMESTAMPTZ,
|
|
due_date TIMESTAMPTZ,
|
|
paid_at TIMESTAMPTZ,
|
|
voided_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_invoices PRIMARY KEY (invoice_id),
|
|
CONSTRAINT fk_invoices_billing_account_id FOREIGN KEY (billing_account_id) REFERENCES core.accounts(billing_account_id),
|
|
CONSTRAINT fk_invoices_subscription_id FOREIGN KEY (subscription_id) REFERENCES core.subscriptions(subscription_id)
|
|
);
|
|
|
|
CREATE INDEX idx_invoices_billing_account_id ON core.invoices(billing_account_id);
|
|
CREATE INDEX idx_invoices_subscription_id ON core.invoices(subscription_id);
|
|
CREATE INDEX idx_invoices_status ON core.invoices(status);
|
|
|
|
CREATE TRIGGER trigger_invoices_updated_at
|
|
BEFORE UPDATE ON core.invoices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.invoice_line_items (
|
|
line_item_id UUID NOT NULL DEFAULT uuidv7(),
|
|
invoice_id UUID NOT NULL,
|
|
subscription_item_id UUID,
|
|
price_id UUID NOT NULL,
|
|
amount INTEGER NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL,
|
|
description TEXT,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
period_start TIMESTAMPTZ,
|
|
period_end TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_invoice_line_items PRIMARY KEY (line_item_id),
|
|
CONSTRAINT fk_invoice_line_items_invoice_id FOREIGN KEY (invoice_id) REFERENCES core.invoices(invoice_id),
|
|
CONSTRAINT fk_invoice_line_items_subscription_item_id FOREIGN KEY (subscription_item_id) REFERENCES core.subscription_items(subscription_item_id),
|
|
CONSTRAINT fk_invoice_line_items_price_id FOREIGN KEY (price_id) REFERENCES core.prices(price_id)
|
|
);
|
|
|
|
CREATE INDEX idx_invoice_line_items_invoice_id ON core.invoice_line_items(invoice_id);
|
|
|
|
CREATE TRIGGER trigger_invoice_line_items_updated_at
|
|
BEFORE UPDATE ON core.invoice_line_items
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- distinct from the dropped legacy public.payments (db/00001/00002 in the
|
|
-- old chain) -- this is the real billing ledger row.
|
|
CREATE TABLE core.payments (
|
|
payment_id UUID NOT NULL DEFAULT uuidv7(),
|
|
invoice_id UUID NOT NULL,
|
|
billing_account_id UUID NOT NULL,
|
|
payment_method_id UUID,
|
|
amount INTEGER NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
failed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_payments PRIMARY KEY (payment_id),
|
|
CONSTRAINT fk_payments_invoice_id FOREIGN KEY (invoice_id) REFERENCES core.invoices(invoice_id),
|
|
CONSTRAINT fk_payments_billing_account_id FOREIGN KEY (billing_account_id) REFERENCES core.accounts(billing_account_id),
|
|
CONSTRAINT fk_payments_payment_method_id FOREIGN KEY (payment_method_id) REFERENCES core.payment_methods(payment_method_id)
|
|
);
|
|
|
|
CREATE INDEX idx_payments_invoice_id ON core.payments(invoice_id);
|
|
CREATE INDEX idx_payments_billing_account_id ON core.payments(billing_account_id);
|
|
CREATE INDEX idx_payments_status ON core.payments(status);
|
|
|
|
CREATE TRIGGER trigger_payments_updated_at
|
|
BEFORE UPDATE ON core.payments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- sole and universal representation of product-ladder ('plan') membership
|
|
-- -- no label column on products (Doc 31 Amendment #3).
|
|
CREATE TABLE core.plan_ladder_tiers (
|
|
plan_ladder_id UUID NOT NULL,
|
|
product_id UUID NOT NULL,
|
|
rank INTEGER NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_plan_ladder_tiers PRIMARY KEY (plan_ladder_id, product_id),
|
|
CONSTRAINT uq_plan_ladder_tiers_plan_ladder_id_rank UNIQUE (plan_ladder_id, rank),
|
|
CONSTRAINT fk_plan_ladder_tiers_plan_ladder_id FOREIGN KEY (plan_ladder_id) REFERENCES core.plan_ladders(plan_ladder_id),
|
|
CONSTRAINT fk_plan_ladder_tiers_product_id FOREIGN KEY (product_id) REFERENCES core.products(product_id)
|
|
);
|
|
|
|
CREATE INDEX idx_plan_ladder_tiers_product_id ON core.plan_ladder_tiers(product_id);
|
|
|
|
CREATE TRIGGER trigger_plan_ladder_tiers_updated_at
|
|
BEFORE UPDATE ON core.plan_ladder_tiers
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.subscription_scheduled_changes (
|
|
scheduled_change_id UUID NOT NULL DEFAULT uuidv7(),
|
|
subscription_id UUID NOT NULL,
|
|
change_type VARCHAR(30) NOT NULL,
|
|
target_price_id UUID,
|
|
target_quantity INTEGER,
|
|
effective_at TIMESTAMPTZ NOT NULL,
|
|
effective_trigger VARCHAR(20) NOT NULL,
|
|
credit_disposition VARCHAR(10),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'scheduled',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_subscription_scheduled_changes PRIMARY KEY (scheduled_change_id),
|
|
CONSTRAINT fk_subscription_scheduled_changes_subscription_id FOREIGN KEY (subscription_id) REFERENCES core.subscriptions(subscription_id),
|
|
CONSTRAINT fk_subscription_scheduled_changes_target_price_id FOREIGN KEY (target_price_id) REFERENCES core.prices(price_id),
|
|
CONSTRAINT chk_ssc_change_type CHECK (change_type IN ('plan_switch', 'quantity_change', 'cancellation')),
|
|
CONSTRAINT chk_ssc_effective_trigger CHECK (effective_trigger IN ('at_date', 'period_end', 'term_boundary')),
|
|
CONSTRAINT chk_ssc_status CHECK (status IN ('scheduled', 'applied', 'superseded', 'canceled')),
|
|
CONSTRAINT chk_ssc_credit_disposition CHECK (credit_disposition IS NULL OR credit_disposition IN ('ledger', 'cash'))
|
|
);
|
|
|
|
CREATE INDEX idx_ssc_subscription_id ON core.subscription_scheduled_changes(subscription_id);
|
|
CREATE INDEX idx_ssc_due ON core.subscription_scheduled_changes(effective_at) WHERE status = 'scheduled';
|
|
|
|
CREATE TRIGGER trigger_subscription_scheduled_changes_updated_at
|
|
BEFORE UPDATE ON core.subscription_scheduled_changes
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Entitlements: pools, grants & provisions
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE core.resource_pools (
|
|
pool_id UUID NOT NULL DEFAULT uuidv7(),
|
|
org_id UUID NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(100) NOT NULL,
|
|
pool_type VARCHAR(20) NOT NULL,
|
|
is_auto_managed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
description TEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
suspended_at TIMESTAMPTZ,
|
|
archived_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_resource_pools PRIMARY KEY (pool_id),
|
|
CONSTRAINT uq_resource_pools_org_id_slug UNIQUE (org_id, slug),
|
|
CONSTRAINT fk_resource_pools_org_id FOREIGN KEY (org_id) REFERENCES core.organizations(org_id)
|
|
);
|
|
|
|
CREATE INDEX idx_resource_pools_org_id ON core.resource_pools(org_id);
|
|
|
|
CREATE TRIGGER trigger_resource_pools_updated_at
|
|
BEFORE UPDATE ON core.resource_pools
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.pool_assignments (
|
|
assignment_id UUID NOT NULL DEFAULT uuidv7(),
|
|
pool_id UUID NOT NULL,
|
|
workspace_id UUID NOT NULL,
|
|
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
suspended_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_pool_assignments PRIMARY KEY (assignment_id),
|
|
CONSTRAINT uq_pool_assignments_pool_id_workspace_id UNIQUE (pool_id, workspace_id),
|
|
CONSTRAINT fk_pool_assignments_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id),
|
|
CONSTRAINT fk_pool_assignments_workspace_id FOREIGN KEY (workspace_id) REFERENCES core.workspaces(workspace_id)
|
|
);
|
|
|
|
CREATE INDEX idx_pool_assignments_workspace_id ON core.pool_assignments(workspace_id);
|
|
CREATE INDEX idx_pool_assignments_pool_id ON core.pool_assignments(pool_id);
|
|
|
|
CREATE TRIGGER trigger_pool_assignments_updated_at
|
|
BEFORE UPDATE ON core.pool_assignments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.entitlement_sets (
|
|
set_id UUID NOT NULL DEFAULT uuidv7(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_entitlement_sets PRIMARY KEY (set_id),
|
|
CONSTRAINT uq_entitlement_sets_name UNIQUE (name)
|
|
);
|
|
|
|
CREATE TRIGGER trigger_entitlement_sets_updated_at
|
|
BEFORE UPDATE ON core.entitlement_sets
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- granted_to_billing_account_id has NO FK -- heterogeneous recipient arc,
|
|
-- see chk_grants_recipient.
|
|
CREATE TABLE core.grants (
|
|
grant_id UUID NOT NULL DEFAULT uuidv7(),
|
|
product_id UUID,
|
|
granted_to_billing_account_id UUID,
|
|
granted_to_org_id UUID,
|
|
granted_to_person_id UUID,
|
|
granted_by_person_id UUID,
|
|
grant_reason VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
valid_until TIMESTAMPTZ,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
revoked_at TIMESTAMPTZ,
|
|
revoked_by_person_id UUID,
|
|
revocation_reason TEXT,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
entitlement_set_id UUID,
|
|
extends_grant_id UUID,
|
|
CONSTRAINT pk_grants PRIMARY KEY (grant_id),
|
|
CONSTRAINT fk_grants_product_id FOREIGN KEY (product_id) REFERENCES core.products(product_id),
|
|
CONSTRAINT fk_grants_granted_to_org_id FOREIGN KEY (granted_to_org_id) REFERENCES core.organizations(org_id),
|
|
CONSTRAINT fk_grants_granted_to_person_id FOREIGN KEY (granted_to_person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_grants_granted_by_person_id FOREIGN KEY (granted_by_person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_grants_revoked_by_person_id FOREIGN KEY (revoked_by_person_id) REFERENCES core.persons(person_id),
|
|
CONSTRAINT fk_grants_entitlement_set_id FOREIGN KEY (entitlement_set_id) REFERENCES core.entitlement_sets(set_id),
|
|
CONSTRAINT fk_grants_extends_grant_id FOREIGN KEY (extends_grant_id) REFERENCES core.grants(grant_id),
|
|
CONSTRAINT chk_grants_recipient CHECK ((granted_to_billing_account_id IS NOT NULL AND granted_to_org_id IS NULL AND granted_to_person_id IS NULL)
|
|
OR (granted_to_billing_account_id IS NULL AND granted_to_org_id IS NOT NULL AND granted_to_person_id IS NULL)
|
|
OR (granted_to_billing_account_id IS NULL AND granted_to_org_id IS NULL AND granted_to_person_id IS NOT NULL)),
|
|
CONSTRAINT chk_grants_system_actor CHECK (granted_by_person_id IS NOT NULL OR grant_reason = 'default'),
|
|
CONSTRAINT chk_grants_entitlement_source CHECK (entitlement_set_id IS NOT NULL OR product_id IS NOT NULL),
|
|
CONSTRAINT chk_grants_no_self_extend CHECK (extends_grant_id IS NULL OR extends_grant_id != grant_id)
|
|
);
|
|
|
|
CREATE INDEX idx_grants_org_id ON core.grants(granted_to_org_id);
|
|
CREATE INDEX idx_grants_product_id ON core.grants(product_id);
|
|
CREATE INDEX idx_grants_extends_grant_id ON core.grants(extends_grant_id) WHERE extends_grant_id IS NOT NULL;
|
|
|
|
CREATE TRIGGER trigger_grants_updated_at
|
|
BEFORE UPDATE ON core.grants
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
CREATE TRIGGER trigger_grant_lineage_same_org
|
|
BEFORE INSERT OR UPDATE ON core.grants
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core.check_grant_lineage_same_org();
|
|
CREATE TRIGGER trigger_grant_lineage_immutable
|
|
BEFORE UPDATE ON core.grants
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core.check_grant_lineage_immutable();
|
|
|
|
-- billing_account_id/subscription_id/purchase_id have NO FK --
|
|
-- heterogeneous source arc, see chk_pool_provisions_source (purchase_id is
|
|
-- a loose slot; no purchases table exists).
|
|
CREATE TABLE core.pool_provisions (
|
|
provision_id UUID NOT NULL DEFAULT uuidv7(),
|
|
pool_id UUID NOT NULL,
|
|
billing_account_id UUID,
|
|
subscription_id UUID,
|
|
purchase_id UUID,
|
|
grant_id UUID,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
suspended_at TIMESTAMPTZ,
|
|
ended_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
entitlement_set_id UUID NOT NULL,
|
|
CONSTRAINT pk_pool_provisions PRIMARY KEY (provision_id),
|
|
CONSTRAINT fk_pool_provisions_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id),
|
|
CONSTRAINT fk_pool_provisions_grant_id FOREIGN KEY (grant_id) REFERENCES core.grants(grant_id),
|
|
CONSTRAINT fk_pool_provisions_entitlement_set_id FOREIGN KEY (entitlement_set_id) REFERENCES core.entitlement_sets(set_id),
|
|
CONSTRAINT chk_pool_provisions_source CHECK ((subscription_id IS NOT NULL AND purchase_id IS NULL AND grant_id IS NULL)
|
|
OR (subscription_id IS NULL AND purchase_id IS NOT NULL AND grant_id IS NULL)
|
|
OR (subscription_id IS NULL AND purchase_id IS NULL AND grant_id IS NOT NULL))
|
|
);
|
|
|
|
CREATE INDEX idx_pool_provisions_pool_id ON core.pool_provisions(pool_id);
|
|
CREATE INDEX idx_pool_provisions_grant_id ON core.pool_provisions(grant_id);
|
|
CREATE INDEX idx_pool_provisions_entitlement_set_id ON core.pool_provisions(entitlement_set_id);
|
|
|
|
CREATE TRIGGER trigger_pool_provisions_updated_at
|
|
BEFORE UPDATE ON core.pool_provisions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
CREATE TRIGGER trigger_sync_pool_provision_ladders
|
|
AFTER UPDATE ON core.pool_provisions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core.sync_pool_provision_ladders();
|
|
|
|
-- pool_id/status/activated_at/ended_at are denormalized from
|
|
-- pool_provisions, kept in sync by the core.sync_pool_provision_ladders()
|
|
-- AFTER UPDATE trigger on pool_provisions. The EXCLUDE constraint requires
|
|
-- btree_gist (see extension near top of file); WHERE (status = 'active')
|
|
-- caps it to at most one active attachment per (pool, ladder) at any
|
|
-- instant.
|
|
CREATE TABLE core.pool_provision_ladders (
|
|
provision_id UUID NOT NULL,
|
|
plan_ladder_id UUID NOT NULL,
|
|
pool_id UUID NOT NULL,
|
|
status VARCHAR(20) NOT NULL,
|
|
activated_at TIMESTAMPTZ NOT NULL,
|
|
ended_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
product_id UUID NOT NULL,
|
|
CONSTRAINT pk_pool_provision_ladders PRIMARY KEY (provision_id, plan_ladder_id),
|
|
CONSTRAINT fk_pool_provision_ladders_provision_id FOREIGN KEY (provision_id) REFERENCES core.pool_provisions(provision_id),
|
|
CONSTRAINT fk_pool_provision_ladders_plan_ladder_id FOREIGN KEY (plan_ladder_id) REFERENCES core.plan_ladders(plan_ladder_id),
|
|
CONSTRAINT fk_pool_provision_ladders_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id),
|
|
CONSTRAINT fk_pool_provision_ladders_product_id FOREIGN KEY (product_id) REFERENCES core.products(product_id),
|
|
CONSTRAINT excl_pool_provision_ladders_active_overlap EXCLUDE USING gist (
|
|
pool_id WITH =,
|
|
plan_ladder_id WITH =,
|
|
tstzrange(activated_at, ended_at, '[)') WITH &&
|
|
) WHERE (status = 'active')
|
|
);
|
|
|
|
CREATE INDEX idx_pool_provision_ladders_pool_ladder ON core.pool_provision_ladders(pool_id, plan_ladder_id);
|
|
CREATE INDEX idx_pool_provision_ladders_plan_ladder ON core.pool_provision_ladders(plan_ladder_id);
|
|
CREATE INDEX idx_pool_provision_ladders_product_id ON core.pool_provision_ladders(product_id);
|
|
|
|
CREATE TRIGGER trigger_pool_provision_ladders_updated_at
|
|
BEFORE UPDATE ON core.pool_provision_ladders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- append-only audit log; no updated_at column, correctly no trigger.
|
|
CREATE TABLE core.pool_provision_transitions (
|
|
transition_id UUID NOT NULL DEFAULT uuidv7(),
|
|
pool_id UUID NOT NULL,
|
|
provision_id UUID,
|
|
plan_ladder_id UUID NOT NULL,
|
|
from_rank INTEGER,
|
|
to_rank INTEGER,
|
|
transition_type VARCHAR(20) NOT NULL,
|
|
actor_type VARCHAR(20) NOT NULL,
|
|
actor_id UUID,
|
|
reason TEXT,
|
|
effective_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_pool_provision_transitions PRIMARY KEY (transition_id),
|
|
CONSTRAINT fk_pool_provision_transitions_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id),
|
|
CONSTRAINT fk_pool_provision_transitions_provision_id FOREIGN KEY (provision_id) REFERENCES core.pool_provisions(provision_id),
|
|
CONSTRAINT fk_pool_provision_transitions_plan_ladder_id FOREIGN KEY (plan_ladder_id) REFERENCES core.plan_ladders(plan_ladder_id),
|
|
CONSTRAINT chk_pool_provision_transitions_type CHECK (transition_type IN ('initiate', 'upgrade', 'downgrade', 'end', 'extend')),
|
|
CONSTRAINT chk_pool_provision_transitions_actor_type CHECK (actor_type IN ('operator', 'system', 'webhook')),
|
|
CONSTRAINT chk_pool_provision_transitions_operator_actor CHECK (actor_type != 'operator' OR actor_id IS NOT NULL)
|
|
);
|
|
|
|
CREATE INDEX idx_pool_provision_transitions_pool_id ON core.pool_provision_transitions(pool_id);
|
|
CREATE INDEX idx_pool_provision_transitions_provision_id ON core.pool_provision_transitions(provision_id);
|
|
CREATE INDEX idx_pool_provision_transitions_plan_ladder_id ON core.pool_provision_transitions(plan_ladder_id);
|
|
CREATE INDEX idx_pool_provision_transitions_effective_at ON core.pool_provision_transitions(effective_at);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Provider registry & numeric entitlements
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- has updated_at but no maintaining trigger anywhere in the old migration
|
|
-- chain -- pre-existing gap, not fixed here.
|
|
CREATE TABLE core.providers (
|
|
slug TEXT NOT NULL,
|
|
provider_kind TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
operator_surface_path TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_providers PRIMARY KEY (slug),
|
|
CONSTRAINT chk_providers_slug_hygiene CHECK (slug ~ '^[a-z0-9]+$'),
|
|
CONSTRAINT chk_providers_kind_valid CHECK (provider_kind IN ('payment', 'provisioning', 'notification', 'tax'))
|
|
);
|
|
|
|
-- no updated_at column, correctly no trigger. provider NULL = platform-
|
|
-- owned/pooled.
|
|
CREATE TABLE core.resource_keys (
|
|
resource_key VARCHAR(100) NOT NULL,
|
|
display_name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
unit VARCHAR(50) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
provider TEXT,
|
|
CONSTRAINT pk_resource_keys PRIMARY KEY (resource_key),
|
|
CONSTRAINT fk_resource_keys_provider FOREIGN KEY (provider) REFERENCES core.providers(slug)
|
|
);
|
|
|
|
CREATE TABLE core.numeric_entitlements (
|
|
entitlement_id UUID NOT NULL DEFAULT uuidv7(),
|
|
pool_id UUID NOT NULL,
|
|
resource_key VARCHAR(100) NOT NULL,
|
|
entitlement_type VARCHAR(20) NOT NULL,
|
|
resource_limit BIGINT NOT NULL DEFAULT 0,
|
|
reset_period VARCHAR(20),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_numeric_entitlements PRIMARY KEY (entitlement_id),
|
|
CONSTRAINT uq_numeric_entitlements_pool_id_resource_key UNIQUE (pool_id, resource_key),
|
|
CONSTRAINT fk_numeric_entitlements_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id),
|
|
CONSTRAINT fk_numeric_entitlements_resource_key FOREIGN KEY (resource_key) REFERENCES core.resource_keys(resource_key)
|
|
);
|
|
|
|
CREATE INDEX idx_numeric_entitlements_pool_id ON core.numeric_entitlements(pool_id);
|
|
|
|
CREATE TRIGGER trigger_numeric_entitlements_updated_at
|
|
BEFORE UPDATE ON core.numeric_entitlements
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.numeric_entitlement_contributions (
|
|
contribution_id UUID NOT NULL DEFAULT uuidv7(),
|
|
entitlement_id UUID NOT NULL,
|
|
provision_id UUID NOT NULL,
|
|
contributed_value BIGINT NOT NULL,
|
|
stacking_policy VARCHAR(20) NOT NULL DEFAULT 'additive',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_numeric_entitlement_contributions PRIMARY KEY (contribution_id),
|
|
CONSTRAINT uq_numeric_entitlement_contributions_scope UNIQUE (entitlement_id, provision_id),
|
|
CONSTRAINT fk_numeric_entitlement_contributions_entitlement_id FOREIGN KEY (entitlement_id) REFERENCES core.numeric_entitlements(entitlement_id),
|
|
CONSTRAINT fk_numeric_entitlement_contributions_provision_id FOREIGN KEY (provision_id) REFERENCES core.pool_provisions(provision_id)
|
|
);
|
|
|
|
CREATE INDEX idx_numeric_entitlement_contributions_entitlement_id ON core.numeric_entitlement_contributions(entitlement_id);
|
|
CREATE INDEX idx_numeric_entitlement_contributions_provision_id ON core.numeric_entitlement_contributions(provision_id);
|
|
|
|
CREATE TRIGGER trigger_numeric_entitlement_contributions_updated_at
|
|
BEFORE UPDATE ON core.numeric_entitlement_contributions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- resource_key is a denormalized copy, no FK of its own. hot-path composite
|
|
-- index for entitlement checks (D11).
|
|
CREATE TABLE core.numeric_entitlement_usage (
|
|
usage_id UUID NOT NULL DEFAULT uuidv7(),
|
|
entitlement_id UUID NOT NULL,
|
|
pool_id UUID NOT NULL,
|
|
resource_key VARCHAR(100) NOT NULL,
|
|
current_usage BIGINT NOT NULL DEFAULT 0,
|
|
current_period_start TIMESTAMPTZ,
|
|
current_period_end TIMESTAMPTZ,
|
|
last_reset_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_numeric_entitlement_usage PRIMARY KEY (usage_id),
|
|
CONSTRAINT uq_numeric_entitlement_usage_entitlement_id UNIQUE (entitlement_id),
|
|
CONSTRAINT fk_numeric_entitlement_usage_entitlement_id FOREIGN KEY (entitlement_id) REFERENCES core.numeric_entitlements(entitlement_id),
|
|
CONSTRAINT fk_numeric_entitlement_usage_pool_id FOREIGN KEY (pool_id) REFERENCES core.resource_pools(pool_id)
|
|
);
|
|
|
|
CREATE INDEX idx_numeric_entitlement_usage_pool_resource ON core.numeric_entitlement_usage(pool_id, resource_key);
|
|
|
|
CREATE TRIGGER trigger_numeric_entitlement_usage_updated_at
|
|
BEFORE UPDATE ON core.numeric_entitlement_usage
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- credit rules (NULL resource_key) and deactivated history are
|
|
-- unconstrained by design.
|
|
CREATE TABLE core.entitlement_set_rules (
|
|
rule_id UUID NOT NULL DEFAULT uuidv7(),
|
|
set_id UUID NOT NULL,
|
|
rule_type VARCHAR(20) NOT NULL,
|
|
resource_key VARCHAR(100),
|
|
resource_value BIGINT,
|
|
resource_per_unit BOOLEAN,
|
|
stacking_policy VARCHAR(20) DEFAULT 'additive',
|
|
reset_period VARCHAR(20),
|
|
credit_amount INTEGER,
|
|
credit_currency VARCHAR(3),
|
|
description TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_entitlement_set_rules PRIMARY KEY (rule_id),
|
|
CONSTRAINT fk_entitlement_set_rules_set_id FOREIGN KEY (set_id) REFERENCES core.entitlement_sets(set_id),
|
|
CONSTRAINT fk_entitlement_set_rules_resource_key FOREIGN KEY (resource_key) REFERENCES core.resource_keys(resource_key),
|
|
CONSTRAINT chk_entitlement_set_rules_type CHECK ((rule_type = 'boolean' AND resource_key IS NOT NULL
|
|
AND resource_value IS NULL AND stacking_policy IS NULL
|
|
AND reset_period IS NULL AND resource_per_unit IS NULL
|
|
AND credit_amount IS NULL AND credit_currency IS NULL)
|
|
OR (rule_type = 'limit' AND resource_key IS NOT NULL AND resource_value IS NOT NULL
|
|
AND reset_period IS NULL
|
|
AND credit_amount IS NULL AND credit_currency IS NULL)
|
|
OR (rule_type = 'quota' AND resource_key IS NOT NULL AND resource_value IS NOT NULL
|
|
AND reset_period IS NOT NULL
|
|
AND credit_amount IS NULL AND credit_currency IS NULL)
|
|
OR (rule_type = 'credit' AND credit_amount IS NOT NULL AND credit_currency IS NOT NULL
|
|
AND resource_key IS NULL AND resource_value IS NULL
|
|
AND stacking_policy IS NULL AND reset_period IS NULL AND resource_per_unit IS NULL))
|
|
);
|
|
|
|
CREATE INDEX idx_entitlement_set_rules_set_id ON core.entitlement_set_rules(set_id);
|
|
CREATE UNIQUE INDEX uq_entitlement_set_rules_active_resource_key ON core.entitlement_set_rules(set_id, resource_key) WHERE is_active = TRUE AND resource_key IS NOT NULL;
|
|
|
|
CREATE TRIGGER trigger_entitlement_set_rules_updated_at
|
|
BEFORE UPDATE ON core.entitlement_set_rules
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TABLE core.provider_operations (
|
|
provider TEXT NOT NULL,
|
|
operation TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_provider_operations PRIMARY KEY (provider, operation),
|
|
CONSTRAINT fk_provider_operations_provider FOREIGN KEY (provider) REFERENCES core.providers(slug) ON DELETE CASCADE,
|
|
CONSTRAINT chk_provider_operations_verb_valid CHECK (operation IN ('create', 'set_status', 'delete', 'list', 'describe'))
|
|
);
|
|
|
|
-- hygiene format, not a closed enum -- per-provider vocabulary.
|
|
CREATE TABLE core.provider_states (
|
|
provider TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_provider_states PRIMARY KEY (provider, state),
|
|
CONSTRAINT fk_provider_states_provider FOREIGN KEY (provider) REFERENCES core.providers(slug) ON DELETE CASCADE,
|
|
CONSTRAINT chk_provider_states_value_hygiene CHECK (state ~ '^[a-z][a-z0-9_]*$')
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Integration infrastructure: webhooks & outbox
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Idempotent inbound webhook capture, partitioned monthly. Processors poll by
|
|
-- status; partitioning keeps old data manageable.
|
|
CREATE TABLE core.webhook_events (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
|
provider TEXT NOT NULL,
|
|
provider_event_id TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}',
|
|
status TEXT NOT NULL DEFAULT 'received',
|
|
retry_count INT NOT NULL DEFAULT 0,
|
|
error_message TEXT,
|
|
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
processed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_webhook_events PRIMARY KEY (id, received_at),
|
|
CONSTRAINT uq_webhook_events_provider_event UNIQUE (provider, provider_event_id, received_at)
|
|
) PARTITION BY RANGE (received_at);
|
|
|
|
-- +goose StatementBegin
|
|
-- Create initial partitions: current month + next 2 months. Future
|
|
-- partitions are created by application boot or a scheduler.
|
|
DO $$
|
|
DECLARE
|
|
m_start DATE;
|
|
m_end DATE;
|
|
part TEXT;
|
|
BEGIN
|
|
FOR i IN 0..2 LOOP
|
|
m_start := DATE_TRUNC('month', NOW()) + (i || ' months')::INTERVAL;
|
|
m_end := m_start + '1 month'::INTERVAL;
|
|
part := 'core.webhook_events_' || TO_CHAR(m_start, 'YYYY_MM');
|
|
EXECUTE FORMAT(
|
|
'CREATE TABLE IF NOT EXISTS %s PARTITION OF core.webhook_events
|
|
FOR VALUES FROM (%L) TO (%L)',
|
|
part, m_start, m_end
|
|
);
|
|
END LOOP;
|
|
END $$;
|
|
-- +goose StatementEnd
|
|
|
|
CREATE INDEX idx_webhook_events_status_received
|
|
ON core.webhook_events (status, received_at)
|
|
WHERE status IN ('received', 'failed');
|
|
CREATE INDEX idx_webhook_events_provider_event
|
|
ON core.webhook_events (provider, provider_event_id);
|
|
|
|
-- has updated_at but no maintaining trigger anywhere in the old migration
|
|
-- chain -- pre-existing gap, not fixed here (same gap as core.providers).
|
|
|
|
-- Transactional outbound actions with retry + dead-letter.
|
|
CREATE TABLE core.outbox (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
|
provider TEXT NOT NULL,
|
|
action_type TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}',
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
attempts INT NOT NULL DEFAULT 0,
|
|
max_attempts INT NOT NULL DEFAULT 5,
|
|
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT pk_outbox PRIMARY KEY (id)
|
|
);
|
|
|
|
CREATE INDEX idx_outbox_pending
|
|
ON core.outbox (status, next_attempt_at)
|
|
WHERE status IN ('pending', 'failed');
|
|
|
|
-- has updated_at but no maintaining trigger (same gap).
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Derived views
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Derived-kind view: consumers asking "is this a plan product?" read this
|
|
-- instead of products.product_type, which has nothing to say for plans
|
|
-- (Doc 31 Amendment #3 -- plan-ness is structural, via plan_ladder_tiers
|
|
-- membership, not a label column).
|
|
CREATE VIEW core.product_kinds AS
|
|
SELECT
|
|
p.product_id,
|
|
(CASE
|
|
WHEN EXISTS (
|
|
SELECT 1 FROM core.plan_ladder_tiers t
|
|
WHERE t.product_id = p.product_id
|
|
) THEN 'plan'
|
|
ELSE p.product_type
|
|
END)::VARCHAR AS product_kind
|
|
FROM core.products p;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Roles (Decision 3): the core role triple only. Integration streams
|
|
-- (fedwiki, stripe) each create and grant their own {slug}_owner/writer/
|
|
-- reader roles in their own migration stream's 00001 instead of here
|
|
-- (Decision 7 -- self-contained ownership; core always runs first (core ->
|
|
-- fedwiki -> stripe, per internal/migrate/sources.go), so core_reader
|
|
-- exists by the time each integration stream's cross-schema reader grant
|
|
-- runs).
|
|
--
|
|
-- Roles are cluster-global, not database-scoped, so creation is guarded:
|
|
-- a second database in the same cluster must not abort this stream.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- +goose StatementBegin
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'core_owner') THEN
|
|
CREATE ROLE core_owner NOLOGIN;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'core_writer') THEN
|
|
CREATE ROLE core_writer NOLOGIN;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'core_reader') THEN
|
|
CREATE ROLE core_reader NOLOGIN;
|
|
END IF;
|
|
END
|
|
$$;
|
|
-- +goose StatementEnd
|
|
|
|
-- Schema-level access.
|
|
GRANT USAGE ON SCHEMA core TO core_reader, core_writer, core_owner;
|
|
GRANT CREATE ON SCHEMA core TO core_owner;
|
|
|
|
-- Table-level privileges (also covers core.product_kinds: GRANT ON ALL
|
|
-- TABLES IN SCHEMA includes views, and the view already exists above).
|
|
GRANT ALL ON ALL TABLES IN SCHEMA core TO core_owner;
|
|
GRANT ALL ON ALL TABLES IN SCHEMA core TO core_writer;
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA core TO core_reader;
|
|
|
|
-- Cross-schema reader inheritance: now that identity/organization/billing/
|
|
-- entitlements/integration are one schema, the five intra-core edges from
|
|
-- the old per-module reader-inheritance pattern dissolve into a no-op and
|
|
-- are not reproduced here. Integration streams' equivalent grants
|
|
-- (`GRANT core_reader TO {slug}_writer` for fedwiki and stripe) live in
|
|
-- their own stream's 00001 (Decision 7).
|
|
|
|
-- member_console (the DSN login role, created by Postgres initdb via
|
|
-- POSTGRES_USER) previously acquired privileges only by being a cluster
|
|
-- superuser -- undocumented in-repo (design.md Decision 3 gap). State its
|
|
-- application-level privileges explicitly; owner roles stay reserved for
|
|
-- migrations. Each integration stream's own `GRANT {slug}_writer TO
|
|
-- member_console` lives in its own stream's 00001 (Decision 7).
|
|
GRANT core_writer TO member_console;
|
|
|
|
-- +goose Down
|
|
|
|
-- +goose StatementBegin
|
|
REVOKE core_writer FROM member_console;
|
|
|
|
REVOKE SELECT ON ALL TABLES IN SCHEMA core FROM core_reader;
|
|
REVOKE ALL ON ALL TABLES IN SCHEMA core FROM core_writer;
|
|
REVOKE ALL ON ALL TABLES IN SCHEMA core FROM core_owner;
|
|
REVOKE CREATE ON SCHEMA core FROM core_owner;
|
|
REVOKE USAGE ON SCHEMA core FROM core_reader, core_writer, core_owner;
|
|
|
|
-- Roles are cluster-global: another database in this cluster may still own
|
|
-- objects or hold grants under them, in which case DROP ROLE raises
|
|
-- dependent_objects_still_exist. Leave such a role standing rather than
|
|
-- failing this database's rollback over another database's state.
|
|
DO $$
|
|
DECLARE
|
|
role_name TEXT;
|
|
BEGIN
|
|
FOREACH role_name IN ARRAY ARRAY['core_reader', 'core_writer', 'core_owner'] LOOP
|
|
BEGIN
|
|
EXECUTE format('DROP ROLE IF EXISTS %I', role_name);
|
|
EXCEPTION WHEN dependent_objects_still_exist THEN
|
|
RAISE NOTICE 'role % is still in use elsewhere in this cluster; left in place', role_name;
|
|
END;
|
|
END LOOP;
|
|
END
|
|
$$;
|
|
|
|
DROP VIEW core.product_kinds;
|
|
-- +goose StatementEnd
|
|
-- +goose StatementBegin
|
|
DROP TABLE IF EXISTS core.outbox CASCADE;
|
|
DROP TABLE IF EXISTS core.webhook_events CASCADE;
|
|
DROP TABLE IF EXISTS core.provider_states CASCADE;
|
|
DROP TABLE IF EXISTS core.provider_operations CASCADE;
|
|
DROP TABLE IF EXISTS core.entitlement_set_rules CASCADE;
|
|
DROP TABLE IF EXISTS core.numeric_entitlement_usage CASCADE;
|
|
DROP TABLE IF EXISTS core.numeric_entitlement_contributions CASCADE;
|
|
DROP TABLE IF EXISTS core.numeric_entitlements CASCADE;
|
|
DROP TABLE IF EXISTS core.resource_keys CASCADE;
|
|
DROP TABLE IF EXISTS core.providers CASCADE;
|
|
DROP TABLE IF EXISTS core.pool_provision_transitions CASCADE;
|
|
DROP TABLE IF EXISTS core.pool_provision_ladders CASCADE;
|
|
DROP TABLE IF EXISTS core.pool_provisions CASCADE;
|
|
DROP TABLE IF EXISTS core.grants CASCADE;
|
|
DROP TABLE IF EXISTS core.entitlement_sets CASCADE;
|
|
DROP TABLE IF EXISTS core.pool_assignments CASCADE;
|
|
DROP TABLE IF EXISTS core.resource_pools CASCADE;
|
|
DROP TABLE IF EXISTS core.subscription_scheduled_changes CASCADE;
|
|
DROP TABLE IF EXISTS core.plan_ladder_tiers CASCADE;
|
|
DROP TABLE IF EXISTS core.payments CASCADE;
|
|
DROP TABLE IF EXISTS core.invoice_line_items CASCADE;
|
|
DROP TABLE IF EXISTS core.invoices CASCADE;
|
|
DROP TABLE IF EXISTS core.payment_methods CASCADE;
|
|
DROP TABLE IF EXISTS core.subscription_changes CASCADE;
|
|
DROP TABLE IF EXISTS core.subscription_items CASCADE;
|
|
DROP TABLE IF EXISTS core.subscriptions CASCADE;
|
|
DROP TABLE IF EXISTS core.accounts CASCADE;
|
|
DROP TABLE IF EXISTS core.role_assignments CASCADE;
|
|
DROP TABLE IF EXISTS core.workspaces CASCADE;
|
|
DROP TABLE IF EXISTS core.org_members CASCADE;
|
|
DROP TABLE IF EXISTS core.roles CASCADE;
|
|
DROP TABLE IF EXISTS core.organizations CASCADE;
|
|
DROP TABLE IF EXISTS core.org_types CASCADE;
|
|
DROP TABLE IF EXISTS core.plan_ladders CASCADE;
|
|
DROP TABLE IF EXISTS core.prices CASCADE;
|
|
DROP TABLE IF EXISTS core.products CASCADE;
|
|
DROP TABLE IF EXISTS core.persons CASCADE;
|
|
DROP TABLE IF EXISTS core.users CASCADE;
|
|
-- +goose StatementEnd
|
|
|
|
-- +goose StatementBegin
|
|
DROP FUNCTION IF EXISTS core.check_grant_lineage_immutable();
|
|
DROP FUNCTION IF EXISTS core.check_grant_lineage_same_org();
|
|
DROP FUNCTION IF EXISTS core.sync_pool_provision_ladders();
|
|
DROP FUNCTION IF EXISTS public.update_updated_at_column();
|
|
-- +goose StatementEnd
|
|
|
|
-- btree_gist is left in place (harmless, potentially shared with other
|
|
-- extensions/roles) -- mirrors the old chain, which never dropped it either.
|
|
DROP SCHEMA IF EXISTS core;
|