-- Account continuity across a rebrand, reissue or issuer change. -- -- WHY THIS EXISTS: one card can appear in `statements` under two completely -- different identities. The live case is the Citi Prestige card, which moved to -- NAB and was rebranded MyCard: -- -- Citibank 5258073330340253 statements ... 2025-11-13 -- NAB 1015001000116227 statements 2025-11-14 ... -- -- Same 14th-to-13th billing cycle, no gap, one continuous product. Nothing in -- the schema linked them, so anything grouping by (bank_name, account) sees a -- dead account and a new one with no history. For the annual-fee detector that -- is not cosmetic: the $700 fee charged 2024-12-13 and 2025-11-13 reads as -- "stopped" when it is due again around November 2026 under the new name. -- -- WHY A TABLE AND NOT AN INFERENCE: contiguous-cycle heuristics would join two -- genuinely different accounts as readily as one renamed one, and a wrong join -- merges two real fee series. There are only a handful of these events in a -- lifetime, each announced by mail, so each link is asserted once with the -- document that proves it. `evidence` is not decoration -- it is how a future -- reader checks the claim instead of trusting it. -- -- NOT `account_owner_mappings`: that answers "whose account is this" and keys on -- the raw (bank_name, account_number). This answers "is this the same account as -- that one", and keys on the normalised form. CREATE TABLE IF NOT EXISTS account_links ( id bigserial PRIMARY KEY, -- Normalised the same way statements.account_number_key is generated -- (non-alphanumerics stripped), so these join without per-call cleanup. -- Amex-style masks keep their X's: they record which digits were redacted. from_bank_name text NOT NULL, from_account_key text NOT NULL, to_bank_name text NOT NULL, to_account_key text NOT NULL, -- When the successor took over. The predecessor's last statement and the -- successor's first are the observable boundary; this is the announced date -- where mail gives one. effective_date date, reason text NOT NULL CHECK (reason IN ('rebrand', 'reissue', 'issuer_change', 'migration')), -- Provenance for the claim, e.g. 'source_documents:32919'. Free text because -- the evidence is not always a document -- sometimes it is a statement pair. evidence text, notes text, created_at timestamptz NOT NULL DEFAULT now(), -- One successor per predecessor: a chain, never a fork. A fork would make -- "the current identity of this account" ambiguous, and the resolver walks -- this forward assuming it is not. CONSTRAINT uq_account_links_from UNIQUE (from_bank_name, from_account_key), -- A link to itself would make that walk non-terminating. CONSTRAINT account_links_no_self CHECK ( from_bank_name IS DISTINCT FROM to_bank_name OR from_account_key IS DISTINCT FROM to_account_key ) ); COMMENT ON TABLE account_links IS 'Predecessor -> successor account identity across rebrands/reissues. Hand-asserted, each row carrying the evidence for its claim.'; CREATE INDEX IF NOT EXISTS account_links_to ON account_links (to_bank_name, to_account_key); -- The one link the mail record proves today. -- source_documents 32919 (2025-10-01) "your Citi Prestige Card account is moving to MyCard" -- source_documents 32618 (2025-11-10) "MyCard is coming soon" -- source_documents 32539 (2025-11-24) "The new MyCard App and MyCard Online are now live" -- Statement boundary: Citibank ends 2025-11-13, NAB begins 2025-11-14. INSERT INTO account_links (from_bank_name, from_account_key, to_bank_name, to_account_key, effective_date, reason, evidence, notes) VALUES ('Citibank', '5258073330340253', 'NAB', '1015001000116227', '2025-11-14', 'issuer_change', 'source_documents:32919,32618,32539', 'Citi Prestige -> MyCard Prestige. Billing cycle (14th-13th) continues unbroken across the boundary.') ON CONFLICT (from_bank_name, from_account_key) DO NOTHING;