Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions apps/api/drizzle/0012_shared_payment_proof_envelope.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
-- P-A: immutable tagged storage only. No payment application contract, permit
-- issuer, source discharge, runtime grant, enrollment or financial effect.
SET LOCAL lock_timeout='10s';
SET LOCAL search_path=pg_catalog,pg_temp;

-- Fail closed if the installed legacy owner key is not the expected exact FK.
DO $$ DECLARE c record; n integer:=0; BEGIN
IF EXISTS(SELECT 1 FROM public.financial_application_manifests m JOIN public.financial_application_roots r ON r.id=m.root_id WHERE r.family<>'REFUND') THEN
RAISE EXCEPTION 'Expected only installed REFUND manifest interpretation'; END IF;
FOR c IN SELECT conname FROM pg_constraint WHERE conrelid='public.financial_application_manifests'::regclass
AND contype='f' AND confrelid='public.webhook_financial_evidence'::regclass AND convalidated AND NOT condeferrable
AND confmatchtype='s' AND confupdtype='a' AND confdeltype='a'
AND conkey=ARRAY(SELECT attnum FROM unnest(ARRAY['inbox_id','proof_revision','payment_id','merchant_id','currency']) WITH ORDINALITY k(name,n)
JOIN pg_attribute ON attrelid='public.financial_application_manifests'::regclass AND attname=k.name ORDER BY k.n)
AND confkey=ARRAY(SELECT attnum FROM unnest(ARRAY['inbox_id','proof_revision','payment_id','merchant_id','currency']) WITH ORDINALITY k(name,n)
JOIN pg_attribute ON attrelid='public.webhook_financial_evidence'::regclass AND attname=k.name ORDER BY k.n)
LOOP
EXECUTE format('ALTER TABLE public.financial_application_manifests DROP CONSTRAINT %I',c.conname); n:=n+1;
END LOOP;
IF n<>1 THEN RAISE EXCEPTION 'Expected exact installed REFUND manifest owner FK'; END IF;
END $$;
ALTER TABLE public.financial_application_manifests
ADD COLUMN proof_kind text NOT NULL DEFAULT 'REFUND_V1',
ADD COLUMN payment_operation_proof_id uuid,
ADD COLUMN legacy_proof_inbox_id uuid GENERATED ALWAYS AS (CASE WHEN proof_kind='REFUND_V1' THEN inbox_id END) STORED,
ADD COLUMN payment_proof_inbox_id uuid GENERATED ALWAYS AS (CASE WHEN proof_kind='PAYMENT_OPERATION_V1' THEN inbox_id END) STORED,
ADD CONSTRAINT application_manifest_proof_branch CHECK(((proof_kind='REFUND_V1' AND payment_operation_proof_id IS NULL)
OR (proof_kind='PAYMENT_OPERATION_V1' AND payment_operation_proof_id IS NOT NULL)) IS TRUE),
ADD CONSTRAINT application_manifest_refund_proof FOREIGN KEY(legacy_proof_inbox_id,proof_revision,payment_id,merchant_id,currency)
REFERENCES public.webhook_financial_evidence(inbox_id,proof_revision,payment_id,merchant_id,currency),
ADD CONSTRAINT application_manifest_payment_proof FOREIGN KEY(payment_proof_inbox_id,payment_operation_proof_id,payment_id,merchant_id,currency)
REFERENCES public.payment_operation_evidence(inbox_id,id,payment_id,merchant_id,currency);
--> statement-breakpoint

-- Interpret the immutable B1 witness, not mutable current capture eligibility.
-- This checker neither admits sources nor certifies a complete application.
CREATE FUNCTION public.check_payment_proof_envelope() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,pg_temp AS $$
DECLARE r public.financial_application_roots%ROWTYPE; e public.payment_operation_evidence%ROWTYPE;
a public.provider_command_authorities%ROWTYPE; auth public.provider_command_authorities%ROWTYPE;
s public.provider_observation_sources%ROWTYPE; receipt public.provider_financial_receipts%ROWTYPE; checked jsonb; baseline jsonb;
BEGIN
-- BEFORE triggers cannot read the new STORED generated linkage values.
SELECT * INTO STRICT r FROM public.financial_application_roots WHERE id=NEW.root_id;
IF NEW.proof_kind='REFUND_V1' THEN
IF r.family<>'REFUND' THEN RAISE EXCEPTION 'REFUND envelope requires REFUND root'; END IF;
RETURN NEW;
END IF;
IF NEW.proof_kind IS DISTINCT FROM 'PAYMENT_OPERATION_V1' THEN RETURN NEW; END IF;
SELECT * INTO STRICT e FROM public.payment_operation_evidence WHERE id=NEW.payment_operation_proof_id AND inbox_id=NEW.inbox_id;
SELECT * INTO STRICT a FROM public.provider_command_authorities WHERE outbox_id=e.outbox_id;
IF NEW.proof_revision IS DISTINCT FROM 1 OR e.proof_version IS DISTINCT FROM 1 OR e.validator_version IS DISTINCT FROM 'PAYMENT_OPERATION_V1'
OR (r.family,r.phase,r.operation_id,r.attempt_id,r.accepted_outbox_id,r.payment_id,r.merchant_id,r.currency,r.provider,r.provider_account,r.policy,r.acceptance_epoch)
IS DISTINCT FROM (a.operation,'TERMINAL',e.attempt_id,e.attempt_id,e.outbox_id,e.payment_id,e.merchant_id,e.currency,e.provider,e.provider_account,e.accounting_policy,e.acceptance_epoch)
OR (NEW.payment_id,NEW.merchant_id,NEW.currency,NEW.policy,NEW.epoch,NEW.proof_operation)
IS DISTINCT FROM (e.payment_id,e.merchant_id,e.currency,e.accounting_policy,e.acceptance_epoch,e.operation)
OR (a.operation_id,a.payment_id,a.merchant_id,a.currency,a.provider,a.provider_account,a.provider_mode,a.accounting_policy,a.acceptance_epoch)
IS DISTINCT FROM (e.attempt_id,e.payment_id,e.merchant_id,e.currency,e.provider,e.provider_account,e.provider_mode,e.accounting_policy,e.acceptance_epoch)
OR (a.operation,e.operation,e.outcome) NOT IN (('AUTHORIZE','payment.authorized','AUTHORIZE_SUCCEEDED'),
('CAPTURE','payment.capture_succeeded','CAPTURE_SUCCEEDED'),('VOID','payment.cancelled','VOID_SUCCEEDED')) THEN
RAISE EXCEPTION 'Payment envelope requires exact canonical root and typed proof'; END IF;
IF e.fingerprint IS DISTINCT FROM sha256(convert_to(e.facts::text,'UTF8'))
OR (e.operation,e.outcome,e.provider,e.provider_account,e.provider_mode,e.attempt_id,e.payment_id,e.merchant_id,e.currency,e.outbox_id,e.execution_id,
e.payment_intent_id,e.charge_id,e.capture_principal,e.authorized_principal,e.accounting_policy,e.acceptance_epoch)
IS DISTINCT FROM (e.facts->>'operation',e.facts->>'outcome',e.facts->>'provider',e.facts->>'provider_account',e.facts->>'provider_mode',
(e.facts->>'attempt_id')::uuid,(e.facts->>'payment_id')::uuid,(e.facts->>'merchant_id')::uuid,e.facts->>'currency',(e.facts->>'outbox_id')::uuid,(e.facts#>>'{source,execution_id}')::uuid,
e.facts->>'payment_intent_id',e.facts->>'charge_id',(e.facts->>'capture_principal')::bigint,(e.facts->>'authorized_principal')::bigint,e.facts->>'accounting_policy',(e.facts->>'acceptance_epoch')::bigint)
OR e.facts->>'validator' IS DISTINCT FROM 'PAYMENT_OPERATION_V1'
OR e.facts->>'interpretation' IS DISTINCT FROM (CASE WHEN a.operation='CAPTURE' THEN 'STRIPE_FIRST_CAPTURE_V1' ELSE 'STRIPE_PAYMENT_WORKFLOW_V1' END)
OR e.facts->>'command_key' IS DISTINCT FROM a.command_key
OR e.facts->>'command_hash' IS DISTINCT FROM encode(sha256(convert_to(a.command_payload::text,'UTF8')),'hex') THEN
RAISE EXCEPTION 'Payment envelope sealed interpretation mismatch'; END IF;
SELECT * INTO STRICT s FROM public.provider_observation_sources WHERE id=e.source_id AND outbox_id=e.outbox_id AND execution_id=e.execution_id;
PERFORM 1 FROM public.provider_command_executions WHERE id=e.execution_id AND outbox_id=e.outbox_id;
IF NOT FOUND THEN RAISE EXCEPTION 'Payment envelope original execution mismatch'; END IF;
checked:=public.checked_payment_command_source(s.id);
IF e.facts->'source' IS DISTINCT FROM checked THEN RAISE EXCEPTION 'Payment envelope original source mismatch'; END IF;
IF a.operation='AUTHORIZE' THEN
IF e.authorization_source_id IS NOT NULL OR e.facts->'authorization' IS DISTINCT FROM 'null'::jsonb THEN
RAISE EXCEPTION 'Authorization envelope cannot adopt a predecessor source'; END IF;
ELSE
baseline:=public.checked_payment_command_source(e.authorization_source_id);
SELECT c.* INTO STRICT auth FROM public.provider_command_authorities c JOIN public.provider_observation_sources v ON v.outbox_id=c.outbox_id WHERE v.id=e.authorization_source_id;
IF e.facts->'authorization' IS DISTINCT FROM baseline OR auth.operation IS DISTINCT FROM 'AUTHORIZE'
OR (auth.payment_id,auth.merchant_id,auth.currency,auth.provider,auth.provider_account,auth.provider_mode)
IS DISTINCT FROM (e.payment_id,e.merchant_id,e.currency,e.provider,e.provider_account,e.provider_mode) THEN
RAISE EXCEPTION 'Payment envelope original authorization mismatch'; END IF;
END IF;
SELECT * INTO STRICT receipt FROM public.provider_financial_receipts WHERE inbox_id=e.inbox_id;
IF (receipt.provider,receipt.provider_account,encode(sha256(receipt.raw_body),'hex'),receipt.verification_version,receipt.normalization_version)
IS DISTINCT FROM (e.provider,e.provider_account,e.facts->>'receipt_hash',e.facts->>'verifier',(e.facts->>'normalization_version')::integer) THEN
RAISE EXCEPTION 'Payment envelope original signed receipt mismatch'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER application_payment_proof BEFORE INSERT ON public.financial_application_manifests
FOR EACH ROW EXECUTE FUNCTION public.check_payment_proof_envelope();

DO $$ DECLARE owner_name text:='fintech_proof_envelope_'||md5(current_database()); BEGIN
EXECUTE format('CREATE ROLE %I NOLOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS',owner_name);
EXECUTE format('GRANT USAGE,CREATE ON SCHEMA public TO %I',owner_name);
EXECUTE format('GRANT SELECT ON public.financial_application_roots,public.payment_operation_evidence,public.provider_command_authorities,
public.provider_command_executions,public.provider_observation_sources,public.provider_financial_receipts TO %I',owner_name);
EXECUTE format('GRANT EXECUTE ON FUNCTION public.checked_payment_command_source(uuid),public.provider_form(bytea),public.provider_form_component(text),
public.payment_json(bytea),public.check_payment_json(json),public.payment_minor(jsonb,text) TO %I',owner_name);
REVOKE ALL ON FUNCTION public.check_payment_proof_envelope() FROM PUBLIC;
EXECUTE format('ALTER FUNCTION public.check_payment_proof_envelope() OWNER TO %I',owner_name);
EXECUTE format('REVOKE CREATE ON SCHEMA public FROM %I',owner_name);
END $$;
7 changes: 7 additions & 0 deletions apps/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"when": 1792540800000,
"tag": "0011_payment_operation_evidence",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1792627200000,
"tag": "0012_shared_payment_proof_envelope",
"breakpoints": true
}
]
}
7 changes: 6 additions & 1 deletion apps/api/src/database/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,12 +528,17 @@ export const financialApplicationManifests = pgTable('financial_application_mani
id: uuid('id').primaryKey().defaultRandom(), rootId: uuid('root_id').notNull().references(() => financialApplicationRoots.id),
transactionId: xid8('transaction_id').notNull(), backendPid: integer('backend_pid').notNull(),
inboxId: uuid('inbox_id').notNull(), proofRevision: integer('proof_revision').notNull(), proofOperation: text('proof_operation').notNull(), claimGeneration: integer('claim_generation').notNull(),
proofKind: text('proof_kind').notNull().default('REFUND_V1'), paymentOperationProofId: uuid('payment_operation_proof_id'),
legacyProofInboxId: uuid('legacy_proof_inbox_id').generatedAlwaysAs(sql`case when proof_kind='REFUND_V1' then inbox_id end`),
paymentProofInboxId: uuid('payment_proof_inbox_id').generatedAlwaysAs(sql`case when proof_kind='PAYMENT_OPERATION_V1' then inbox_id end`),
merchantId: uuid('merchant_id').notNull(), currency: text('currency').notNull(), paymentId: uuid('payment_id').notNull(),
policy: text('policy').notNull(), epoch: allocationMoney('epoch').notNull(), scopeRevision: allocationMoney('scope_revision').notNull(), intakeRevision: allocationMoney('intake_revision').notNull(),
contractVersion: integer('contract_version').notNull(), result: text('result').notNull(), sourceManifest: jsonb('source_manifest').notNull(), effectManifest: jsonb('effect_manifest').notNull(), createdAt: createdAt(),
}, t => [uniqueIndex('application_manifest_transaction').on(t.transactionId,t.backendPid), uniqueIndex('application_manifest_root').on(t.id,t.rootId),
foreignKey({ columns: [t.rootId,t.merchantId,t.currency], foreignColumns: [financialApplicationRoots.id,financialApplicationRoots.merchantId,financialApplicationRoots.currency] }),
foreignKey({ columns: [t.inboxId,t.proofRevision,t.paymentId,t.merchantId,t.currency], foreignColumns: [webhookFinancialEvidence.inboxId,webhookFinancialEvidence.proofRevision,webhookFinancialEvidence.paymentId,webhookFinancialEvidence.merchantId,webhookFinancialEvidence.currency] })]);
check('application_manifest_proof_branch',sql`((${t.proofKind}='REFUND_V1' and ${t.paymentOperationProofId} is null) or (${t.proofKind}='PAYMENT_OPERATION_V1' and ${t.paymentOperationProofId} is not null)) is true`),
foreignKey({ name:'application_manifest_refund_proof',columns: [t.legacyProofInboxId,t.proofRevision,t.paymentId,t.merchantId,t.currency], foreignColumns: [webhookFinancialEvidence.inboxId,webhookFinancialEvidence.proofRevision,webhookFinancialEvidence.paymentId,webhookFinancialEvidence.merchantId,webhookFinancialEvidence.currency] }),
foreignKey({ name:'application_manifest_payment_proof',columns: [t.paymentProofInboxId,t.paymentOperationProofId,t.paymentId,t.merchantId,t.currency],foreignColumns:[paymentOperationEvidence.inboxId,paymentOperationEvidence.id,paymentOperationEvidence.paymentId,paymentOperationEvidence.merchantId,paymentOperationEvidence.currency] })]);
export const financialApplicationPermits = pgTable('financial_application_permits', {
transactionId: xid8('transaction_id').notNull(), backendPid: integer('backend_pid').notNull(), manifestId: uuid('manifest_id').notNull().unique().references(() => financialApplicationManifests.id),
databasePrincipal: customType<{ data: string }>({ dataType: () => 'name' })('database_principal').notNull().default(sql`session_user`),
Expand Down
14 changes: 7 additions & 7 deletions apps/api/test/integration/capture-accounting-migration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
return target;
}
it('installs 0000–0009 on an empty database; repeated migrator execution is a no-op', async () => {
const database = await isolated(); await apply(database,folder);
const before = await fingerprint(database); await apply(database,folder);
const database = await isolated(); await apply(database,historicalFolder(12));
const before = await fingerprint(database); await apply(database,historicalFolder(12));
expect(await fingerprint(database)).toEqual(before);
const [row] = await database.sql`select (select count(*)::integer from drizzle.__drizzle_migrations) as migrations,
(select count(*)::integer from capture_accounting_lots) as lots,(select count(*)::integer from accounting_exceptions) as exceptions`;
Expand All @@ -66,7 +66,7 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
const database = await isolated(); await apply(database,historicalFolder(5));
const f = await captureFixture(database);
await database.sql`insert into capture_accounting_scopes (merchant_id,currency) values (${f.merchantId},'USD')`;
const before = await fingerprint(database); await apply(database,folder); await apply(database,folder);
const before = await fingerprint(database); await apply(database,historicalFolder(12)); await apply(database,historicalFolder(12));
expect(await fingerprint(database)).toEqual(before);
const labels = await database.sql<{enumlabel:string}[]>`select enumlabel from pg_enum where enumtypid='public.inbox_status'::regtype and enumlabel='ACCOUNTING_EXCEPTION'`;
expect(labels).toEqual([{enumlabel:'ACCOUNTING_EXCEPTION'}]);
Expand Down Expand Up @@ -117,7 +117,7 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
]}));
const before = await fingerprint(database);
const functionsBefore = await database.sql<{proname:string;definition:string}[]>`select proname,pg_get_functiondef(oid) as definition from pg_proc where pronamespace='public'::regnamespace order by proname`;
await apply(database,folder); expect(await fingerprint(database)).toEqual(before);
await apply(database,historicalFolder(12)); expect(await fingerprint(database)).toEqual(before);
const [orphanException] = await database.sql`select status,observed_conflict->>'journal_id' as journal from accounting_exceptions where evidence_key=${`legacy-journal:${orphan}`}`;
expect(orphanException).toMatchObject({status:'OPEN',journal:orphan});
for (const c of cases) {
Expand All @@ -139,7 +139,7 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
const [allocations] = await database.sql`select (select count(*)::integer from refund_capture_allocations) as refunds,(select count(*)::integer from dispute_capture_allocations) as disputes`;
expect(allocations).toMatchObject({refunds:0,disputes:0});
const [countsBefore] = await database.sql`select count(*)::integer as exceptions from accounting_exceptions`;
await apply(database,folder); expect(await fingerprint(database)).toEqual(before);
await apply(database,historicalFolder(12)); expect(await fingerprint(database)).toEqual(before);
const [countsAfter] = await database.sql`select count(*)::integer as exceptions from accounting_exceptions`; expect(countsAfter).toEqual(countsBefore);
},30000);
it('classifies NULL-owned legacy pending entries for review while backfilling a valid capture', async () => {
Expand All @@ -158,7 +158,7 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
{accountCode:'PLATFORM_FEE_REVENUE',merchantId:null,credit:300},
]});
});
const before = await fingerprint(database); await apply(database,folder);
const before = await fingerprint(database); await apply(database,historicalFolder(12));
expect(await fingerprint(database)).toEqual(before);
const [invalid] = await database.sql`select original_valid,classification from capture_accounting_legacy_inventory where capture_attempt_id=${attemptId}`;
expect(invalid).toEqual({original_valid:false,classification:'REVIEW_REQUIRED'});
Expand All @@ -181,7 +181,7 @@ describeMigration('B1 migration freshness and conservative legacy upgrade', () =
const before = await fingerprint(database);
// Drizzle wraps the original PostgreSQL exception; assert its cause too.
let rejection: unknown;
try { await apply(database,folder); } catch (error: unknown) { rejection=error; }
try { await apply(database,historicalFolder(12)); } catch (error: unknown) { rejection=error; }
expect(rejection).toBeDefined();
expect((rejection as {cause:Error}).cause.message).toMatch(/Unscoped legacy CAPTURE/i);
expect(await fingerprint(database)).toEqual(before);
Expand Down
Loading
Loading