mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
import { Column, Entity, Index } from "typeorm";
|
|
import { BaseEntity } from "@edr/api-common";
|
|
import {
|
|
ClientAction,
|
|
PaymentReferenceType,
|
|
PaymentService,
|
|
ProviderMethod,
|
|
ProviderPaymentStatus,
|
|
} from "@edr/types";
|
|
|
|
/**
|
|
* One payment attempt for one domain order — the platform-wide source of truth for payment
|
|
* state. `reference_id` is a soft reference into the owning app's schema (never a FK; see
|
|
* docs/payment-service/architecture.md §5).
|
|
*
|
|
* Enum-valued columns are stored as varchar (values mirror the shared @edr/types enums) so
|
|
* adding a provider/status never needs an ALTER TYPE migration.
|
|
*/
|
|
@Entity({ name: "payment_intent" })
|
|
// A domain order may have MANY intents (free method changes; a second real payment is stored as
|
|
// its own SUCCEEDED row). Plain lookup index — no uniqueness. Confirm-once is enforced by the
|
|
// owning app confirming the booking only while it is still payable, not by the DB.
|
|
@Index("idx_payment_intent_reference", ["service", "referenceType", "referenceId"])
|
|
@Index("idx_payment_intent_sweep", ["status", "updatedAt"])
|
|
@Index("idx_payment_intent_idempotency", ["service", "idempotencyKey"])
|
|
export class PaymentIntent extends BaseEntity {
|
|
/** Owning domain app — routing discriminator for notifications. */
|
|
@Column({ name: "service", type: "varchar", length: 16 })
|
|
service!: PaymentService;
|
|
|
|
@Column({ name: "reference_type", type: "varchar", length: 32 })
|
|
referenceType!: PaymentReferenceType;
|
|
|
|
/** Domain order id (booking/shipment). Soft reference — no cross-schema FK. */
|
|
@Column({ name: "reference_id", type: "varchar", length: 64 })
|
|
referenceId!: string;
|
|
|
|
/** Provider-facing reference, prefixed PSG-/FRT- so webhooks route before a DB lookup. */
|
|
@Column({
|
|
name: "merchant_order_id",
|
|
type: "varchar",
|
|
length: 64,
|
|
unique: true,
|
|
})
|
|
merchantOrderId!: string;
|
|
|
|
@Column({ name: "provider", type: "varchar", length: 16 })
|
|
provider!: ProviderMethod;
|
|
|
|
/** Provider-side order/session id (prepay id, HPP orderId, …). */
|
|
@Column({
|
|
name: "provider_order_id",
|
|
type: "varchar",
|
|
length: 128,
|
|
nullable: true,
|
|
})
|
|
providerOrderId?: string | null;
|
|
|
|
/** Final provider transaction id, set on terminal success. */
|
|
@Index("idx_payment_intent_provider_txn")
|
|
@Column({
|
|
name: "provider_txn_id",
|
|
type: "varchar",
|
|
length: 128,
|
|
nullable: true,
|
|
})
|
|
providerTxnId?: string | null;
|
|
|
|
/** App-asserted authoritative amount (real/major price; may be fractional, e.g. 10.50). */
|
|
@Column({ name: "amount_minor", type: "double precision" })
|
|
amountMinor!: number;
|
|
|
|
/** Provider-reported amount; reconciled against amount_minor (e.g. Waafi truncates decimals). */
|
|
@Column({
|
|
name: "confirmed_amount_minor",
|
|
type: "double precision",
|
|
nullable: true,
|
|
})
|
|
confirmedAmountMinor?: number | null;
|
|
|
|
@Column({ name: "currency", type: "varchar", length: 8 })
|
|
currency!: string;
|
|
|
|
/** State machine: REQUIRES_ACTION → PROCESSING → SUCCEEDED | FAILED | CANCELLED (absorbing). */
|
|
@Column({
|
|
name: "status",
|
|
type: "varchar",
|
|
length: 24,
|
|
default: ProviderPaymentStatus.REQUIRES_ACTION,
|
|
})
|
|
status!: ProviderPaymentStatus;
|
|
|
|
/** Redirect/launch payload returned to the app for the user to complete payment. */
|
|
@Column({ name: "client_action", type: "jsonb", nullable: true })
|
|
clientAction?: ClientAction | null;
|
|
|
|
@Column({ name: "failure_code", type: "varchar", length: 64, nullable: true })
|
|
failureCode?: string | null;
|
|
|
|
@Column({ name: "failure_message", type: "text", nullable: true })
|
|
failureMessage?: string | null;
|
|
|
|
/** Caller-supplied initiate dedupe key (in addition to the per-reference upsert). */
|
|
@Column({
|
|
name: "idempotency_key",
|
|
type: "varchar",
|
|
length: 128,
|
|
nullable: true,
|
|
})
|
|
idempotencyKey?: string | null;
|
|
|
|
/**
|
|
* CBE_BILL only: the short numeric Bill_Id the customer types at a CBE channel
|
|
* (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §5). Null for every other provider.
|
|
*/
|
|
@Column({
|
|
name: "bill_reference",
|
|
type: "varchar",
|
|
length: 32,
|
|
nullable: true,
|
|
unique: true,
|
|
})
|
|
billReference?: string | null;
|
|
|
|
/** Payer full name snapshot — fallback for CBE /cbe/query Full_Name when bill-query is down. */
|
|
@Column({ name: "payer_name", type: "varchar", length: 128, nullable: true })
|
|
payerName?: string | null;
|
|
|
|
@Column({ name: "expires_at", type: "timestamptz", nullable: true })
|
|
expiresAt?: Date | null;
|
|
|
|
@Column({ name: "paid_at", type: "timestamptz", nullable: true })
|
|
paidAt?: Date | null;
|
|
|
|
/** Audit copy of the provider initiation request/response (secrets redacted upstream). */
|
|
@Column({ name: "raw_initiation", type: "jsonb", nullable: true })
|
|
rawInitiation?: Record<string, unknown> | null;
|
|
}
|
|
|
|
/** Statuses that keep the per-reference unique index "active" (block a new intent). */
|
|
export const ACTIVE_INTENT_STATUSES = [
|
|
ProviderPaymentStatus.REQUIRES_ACTION,
|
|
ProviderPaymentStatus.PROCESSING,
|
|
ProviderPaymentStatus.SUCCEEDED,
|
|
] as const;
|
|
|
|
export const TERMINAL_INTENT_STATUSES = [
|
|
ProviderPaymentStatus.SUCCEEDED,
|
|
ProviderPaymentStatus.FAILED,
|
|
ProviderPaymentStatus.CANCELLED,
|
|
] as const;
|