Files
edr-platform/apps/edr-payment-api/src/migrations/1781136000000-InitPaymentSchema.ts
2026-06-11 15:25:24 +03:00

125 lines
5.5 KiB
TypeScript

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<void> {
// 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<void> {
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"`);
}
}