Merge branch 'feat/payment-microservice' of github.com:Tria-plc/edr-platform into freight_feature/payments

This commit is contained in:
marshal
2026-06-14 02:38:19 +03:00
192 changed files with 15254 additions and 5671 deletions

View File

@@ -1,4 +1,5 @@
export * from "./payments";
export * from "./payment-messaging";
export interface BaseEntity {
id: string;

View File

@@ -0,0 +1,55 @@
/* ------------------------------------------------------------------------------------------------
* Payment event messaging contract (RabbitMQ)
*
* The single source of truth for the broker topology shared between the payment microservice
* (publisher) and the domain apps (consumers). Both sides import these constants/helpers so the
* exchange name, routing keys, and queue names can never drift apart.
*
* Topology (see docs/payment-service/rabbitmq/):
* exchange payment.events (topic, durable) ← every payment event is published here
* exchange payment.events.dlx (topic, durable) ← dead-letter for events a consumer rejects
* routing payment.<service>.<outcome> e.g. payment.passenger.succeeded
* queue <service>.payment-events bound to payment.<service>.*
* queue <service>.payment-events.dlq dead-letter queue (bound on the dlx)
* ---------------------------------------------------------------------------------------------- */
import { PaymentEventType, PaymentService } from "./payments";
/** Topic exchange every payment event is published to. */
export const PAYMENT_EVENTS_EXCHANGE = "payment.events";
/** Dead-letter exchange for payment events a consumer could not process (poison messages). */
export const PAYMENT_EVENTS_DLX = "payment.events.dlx";
/**
* Routing key for a payment event: `payment.<service>.<outcome>`.
* e.g. `payment.passenger.succeeded`, `payment.freight.failed`.
*/
export function paymentRoutingKey(
service: PaymentService,
eventType: PaymentEventType,
): string {
// "payment.succeeded" -> "succeeded", "payment.failed" -> "failed"
const outcome = eventType.split(".")[1];
return `payment.${service.toLowerCase()}.${outcome}`;
}
/** Binding pattern a service's queue uses so it receives only its own events. */
export function paymentServiceBindingPattern(service: PaymentService): string {
return `payment.${service.toLowerCase()}.*`;
}
/** Durable queue names per owning service: the main work queue and its dead-letter queue. */
export const PAYMENT_QUEUES: Record<
PaymentService,
{ main: string; dlq: string }
> = {
[PaymentService.PASSENGER]: {
main: "passenger.payment-events",
dlq: "passenger.payment-events.dlq",
},
[PaymentService.FREIGHT]: {
main: "freight.payment-events",
dlq: "freight.payment-events.dlq",
},
};

View File

@@ -43,8 +43,17 @@ export interface ProviderInitiationInput {
amountMinor: number;
currency: string;
platform?: PaymentPlatform;
returnUrl?: string
redirectUrl?: string
/**
* Payer account identifier (e.g. mobile-wallet MSISDN in full international format).
* Optional and provider-specific: some wallet providers (e.g. Waafi HPP with
* MWALLET_ACCOUNT) require the payer's phone number up front to pre-fill the hosted page.
*/
payerAccount?: string;
/** Optional caller-supplied redirect targets for redirect/HPP-style providers. */
returnUrl?: string;
redirectUrl?: string;
/** Where the browser lands when the hosted page fails/cancels (UX only — never trusted). */
failureUrl?: string;
}
export interface ProviderInitiationResult {
@@ -67,3 +76,103 @@ export interface PaymentProvider {
initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult>;
queryStatus(merchantOrderId: string): Promise<ProviderStatus>;
}
/* ------------------------------------------------------------------------------------------------
* Payment microservice contracts (docs/payment-service)
*
* Shared shapes exchanged between the payment microservice (apps/edr-payment-api) and the
* domain apps (passenger/freight). Both sides import these so the wire format cannot drift.
* ---------------------------------------------------------------------------------------------- */
/** Which domain app owns the order being paid for. Routing discriminator on every intent. */
export enum PaymentService {
PASSENGER = "PASSENGER",
FREIGHT = "FREIGHT",
}
/** What kind of domain order the intent references (soft reference — never a cross-schema FK). */
export enum PaymentReferenceType {
BOOKING = "BOOKING",
SHIPMENT = "SHIPMENT",
}
/** Body of `POST /payments/initiate` on the payment service (internal, service-authenticated). */
export interface InitiatePaymentRequest {
service: PaymentService;
referenceType: PaymentReferenceType;
/** Domain order id (booking/shipment id). Soft reference; the app has already validated it. */
referenceId: string;
/** Human-readable order ref (e.g. booking ref) shown on provider pages. Defaults to referenceId. */
orderRef?: string;
/** App-asserted authoritative amount in minor units (computed server-side by the domain app). */
amountMinor: number;
currency: string;
provider: ProviderMethod;
platform?: PaymentPlatform;
payerAccount?: string;
/**
* Where the provider's hosted page sends the BROWSER back after success — each calling app
* passes its own UI URL (passenger portal vs freight portal). Per-transaction and UX-only:
* the redirect never confirms payment (only the webhook / status query does), so per-app
* values are safe even though the server-to-server webhook URL is one per merchant.
* Falls back to the payment service's provider config when omitted.
*/
returnUrl?: string;
/** Failure/cancel counterpart of returnUrl. */
failureUrl?: string;
/** Optional caller key to dedupe retried initiations beyond the per-reference upsert. */
idempotencyKey?: string;
}
/** Response of `POST /payments/initiate` and shape of intent lookups. */
export interface PaymentIntentSnapshot {
intentId: string;
service: PaymentService;
referenceType: PaymentReferenceType;
referenceId: string;
merchantOrderId: string;
provider: ProviderMethod;
status: ProviderPaymentStatus;
amountMinor: number;
currency: string;
clientAction?: ClientAction;
providerTxnId?: string;
paidAt?: string;
failureCode?: string;
failureMessage?: string;
expiresAt?: string;
}
export type PaymentEventType = "payment.succeeded" | "payment.failed";
/** Versioned envelope delivered (at-least-once) to the owning app's mark-paid consumer. */
interface PaymentEventBase {
version: 1;
/** Outbox row id — stable across redeliveries; consumers may use it as a dedupe key. */
eventId: string;
eventType: PaymentEventType;
occurredAt: string;
service: PaymentService;
intentId: string;
referenceType: PaymentReferenceType;
referenceId: string;
merchantOrderId: string;
provider: ProviderMethod;
amountMinor: number;
currency: string;
}
export interface PaymentSucceededEvent extends PaymentEventBase {
eventType: "payment.succeeded";
providerTxnId?: string;
paidAt: string;
}
export interface PaymentFailedEvent extends PaymentEventBase {
eventType: "payment.failed";
failureCode?: string;
failureMessage?: string;
}
export type PaymentEvent = PaymentSucceededEvent | PaymentFailedEvent;