import { MigrationInterface, QueryRunner } from "typeorm"; /** * Initial edr_payment schema: payment_intent, payment_webhook_event, notification_outbox. * * Enum-valued columns are varchar on purpose (values mirror the @edr/types enums) so new * providers/statuses never need an ALTER TYPE. uuid defaults use gen_random_uuid() (built into * Postgres 13+; no extension required). */ export class InitPaymentSchema1781136000000 implements MigrationInterface { name = "InitPaymentSchema1781136000000"; public async up(queryRunner: QueryRunner): Promise { // Defensive — bootstrap (ensure-schema) normally creates this before migrations run. await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS "edr_payment"`); await queryRunner.query(` CREATE TABLE "edr_payment"."payment_intent" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz, "service" varchar(16) NOT NULL, "reference_type" varchar(16) NOT NULL, "reference_id" varchar(64) NOT NULL, "merchant_order_id" varchar(64) NOT NULL, "provider" varchar(16) NOT NULL, "provider_order_id" varchar(128), "provider_txn_id" varchar(128), "amount_minor" integer NOT NULL, "confirmed_amount_minor" integer, "currency" varchar(8) NOT NULL, "status" varchar(24) NOT NULL DEFAULT 'REQUIRES_ACTION', "client_action" jsonb, "failure_code" varchar(64), "failure_message" text, "idempotency_key" varchar(128), "expires_at" timestamptz, "paid_at" timestamptz, "raw_initiation" jsonb, CONSTRAINT "pk_payment_intent" PRIMARY KEY ("id"), CONSTRAINT "uq_payment_intent_merchant_order_id" UNIQUE ("merchant_order_id") ) `); // One ACTIVE intent per domain order; terminal-failed attempts remain as audit rows. await queryRunner.query(` CREATE UNIQUE INDEX "uq_payment_intent_active_reference" ON "edr_payment"."payment_intent" ("service", "reference_type", "reference_id") WHERE status NOT IN ('FAILED','CANCELLED') AND deleted_at IS NULL `); await queryRunner.query(` CREATE INDEX "idx_payment_intent_provider_txn" ON "edr_payment"."payment_intent" ("provider_txn_id") `); await queryRunner.query(` CREATE INDEX "idx_payment_intent_sweep" ON "edr_payment"."payment_intent" ("status", "updated_at") `); await queryRunner.query(` CREATE INDEX "idx_payment_intent_idempotency" ON "edr_payment"."payment_intent" ("service", "idempotency_key") `); await queryRunner.query(` CREATE TABLE "edr_payment"."payment_webhook_event" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz, "provider" varchar(16) NOT NULL, "external_event_id" varchar(191) NOT NULL, "merchant_order_id" varchar(64), "provider_txn_id" varchar(128), "signature_valid" boolean NOT NULL DEFAULT false, "status" varchar(64), "payload" jsonb NOT NULL, "received_at" timestamptz NOT NULL DEFAULT now(), "processed_at" timestamptz, "processing_error" text, CONSTRAINT "pk_payment_webhook_event" PRIMARY KEY ("id") ) `); // The webhook dedupe key: duplicate provider deliveries hit this and short-circuit. await queryRunner.query(` CREATE UNIQUE INDEX "uq_payment_webhook_event_external" ON "edr_payment"."payment_webhook_event" ("provider", "external_event_id") `); await queryRunner.query(` CREATE TABLE "edr_payment"."notification_outbox" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz, "event_type" varchar(32) NOT NULL, "service" varchar(16) NOT NULL, "intent_id" uuid NOT NULL, "reference_type" varchar(16) NOT NULL, "reference_id" varchar(64) NOT NULL, "payload" jsonb NOT NULL, "status" varchar(16) NOT NULL DEFAULT 'PENDING', "attempts" integer NOT NULL DEFAULT 0, "next_retry_at" timestamptz, "last_error" text, "sent_at" timestamptz, CONSTRAINT "pk_notification_outbox" PRIMARY KEY ("id") ) `); await queryRunner.query(` CREATE INDEX "idx_notification_outbox_relay" ON "edr_payment"."notification_outbox" ("status", "next_retry_at") `); await queryRunner.query(` CREATE INDEX "idx_notification_outbox_intent" ON "edr_payment"."notification_outbox" ("intent_id") `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP TABLE "edr_payment"."notification_outbox"`); await queryRunner.query(`DROP TABLE "edr_payment"."payment_webhook_event"`); await queryRunner.query(`DROP TABLE "edr_payment"."payment_intent"`); } }