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.
86 lines
3.8 KiB
SQL
86 lines
3.8 KiB
SQL
-- +goose Up
|
|
|
|
-- invoice-numbers (design.md D1-D4; upstream data model v16.2, identifier
|
|
-- policy arbitration 2026-08-29): core.invoices gains a platform-assigned
|
|
-- reference number, scoped per billing account, assigned from a
|
|
-- transactional gapless counter on core.accounts at issuance
|
|
-- (AssignNextInvoiceNumber: UPDATE ... SET next_invoice_number =
|
|
-- next_invoice_number + 1 ... RETURNING next_invoice_number - 1, never a
|
|
-- Postgres sequence -- a sequence burns values on rollback, and gapless
|
|
-- numbering of issued invoices is statutory in several jurisdictions).
|
|
--
|
|
-- This reworks the first cut (2026-08-28), which stored Stripe's own
|
|
-- customer-facing number under a single global partial unique index. That
|
|
-- mechanism is superseded, not amended: the composite per-account index
|
|
-- below replaces the global one, and the format changes from Stripe's
|
|
-- "A1B2C3D4-0001" to the platform's own zero-padded decimal, assigned at
|
|
-- issuance rather than copied from Stripe. Stripe's number moves to
|
|
-- stripe.invoice_mappings.stripe_invoice_number (Stripe-stream migration)
|
|
-- as an external reference.
|
|
--
|
|
-- Grandfathering: any invoice_number already stored under the first cut
|
|
-- (quoted to a member as Stripe's number) is kept verbatim by the backfill
|
|
-- below -- it is never touched, never renumbered, and never counted against
|
|
-- an account's counter. Every account's counter starts at 1 and advances
|
|
-- only past the rows this migration backfills for it, so an account may
|
|
-- show both a grandfathered Stripe-format number and the platform's own
|
|
-- decimal form side by side for a while. The composite unique index
|
|
-- tolerates this: "0001" and "A1B2C3D4-0001" cannot collide.
|
|
|
|
-- The column itself, and its nullability rationale, are unchanged from the
|
|
-- first cut: nullable because drafts never carry a number, and (as of this
|
|
-- rework) a row only gains one at issuance via the account's counter below.
|
|
ALTER TABLE core.invoices
|
|
ADD COLUMN invoice_number VARCHAR(50);
|
|
|
|
ALTER TABLE core.accounts
|
|
ADD COLUMN next_invoice_number INTEGER NOT NULL DEFAULT 1;
|
|
|
|
CREATE UNIQUE INDEX uq_invoices_account_invoice_number
|
|
ON core.invoices (billing_account_id, invoice_number) WHERE invoice_number IS NOT NULL;
|
|
|
|
-- Backfill: every non-draft invoice that still lacks a number (grandfathered
|
|
-- rows already have one and are left untouched) gets one, in created_at
|
|
-- order per billing account, and that account's counter is advanced past
|
|
-- the numbers just assigned here -- never past a grandfathered row's value,
|
|
-- which is not a sequential integer and is not counted.
|
|
CREATE TEMP TABLE invoice_number_backfill ON COMMIT DROP AS
|
|
SELECT invoice_id, billing_account_id,
|
|
row_number() OVER (PARTITION BY billing_account_id ORDER BY created_at, invoice_id) AS n
|
|
FROM core.invoices
|
|
WHERE invoice_number IS NULL AND status <> 'draft';
|
|
|
|
UPDATE core.invoices i
|
|
SET invoice_number = lpad(b.n::text, 4, '0'),
|
|
updated_at = NOW()
|
|
FROM invoice_number_backfill b
|
|
WHERE i.invoice_id = b.invoice_id;
|
|
|
|
UPDATE core.accounts a
|
|
SET next_invoice_number = c.max_n + 1
|
|
FROM (
|
|
SELECT billing_account_id, max(n) AS max_n
|
|
FROM invoice_number_backfill
|
|
GROUP BY billing_account_id
|
|
) c
|
|
WHERE a.billing_account_id = c.billing_account_id;
|
|
|
|
-- Added after the backfill so the backfill itself never has to satisfy it
|
|
-- mid-flight: every non-draft row now carries a number, so the CHECK is
|
|
-- true for the entire table from the moment it is added.
|
|
ALTER TABLE core.invoices
|
|
ADD CONSTRAINT chk_invoices_issued_have_number CHECK (status = 'draft' OR invoice_number IS NOT NULL);
|
|
|
|
-- +goose Down
|
|
|
|
ALTER TABLE core.invoices
|
|
DROP CONSTRAINT chk_invoices_issued_have_number;
|
|
|
|
DROP INDEX core.uq_invoices_account_invoice_number;
|
|
|
|
ALTER TABLE core.accounts
|
|
DROP COLUMN next_invoice_number;
|
|
|
|
CREATE UNIQUE INDEX uq_invoices_invoice_number
|
|
ON core.invoices (invoice_number) WHERE invoice_number IS NOT NULL;
|