mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -29,6 +29,11 @@ export class PaymentEventsConsumer {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
async handle(event: PaymentEvent): Promise<Nack | void> {
|
async handle(event: PaymentEvent): Promise<Nack | void> {
|
||||||
|
// Logged the instant RabbitMQ delivers the message, before any DB work — proves the
|
||||||
|
// payment -> passenger broker connection works even if processing later fails/hangs.
|
||||||
|
this.logger.log(
|
||||||
|
`RECEIVED ${event.eventType} (${event.eventId}) ref=${event.referenceId} via RabbitMQ`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const result = await this.paymentsService.handlePaymentEvent(
|
const result = await this.paymentsService.handlePaymentEvent(
|
||||||
event as unknown as PaymentEventDto,
|
event as unknown as PaymentEventDto,
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsIn,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsPositive,
|
||||||
|
IsString,
|
||||||
|
} from "class-validator";
|
||||||
|
import {
|
||||||
|
PaymentEventType,
|
||||||
|
PaymentReferenceType,
|
||||||
|
PaymentService,
|
||||||
|
ProviderMethod,
|
||||||
|
} from "@edr/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for the dev-only POST /test/payment-event endpoint. Every field is optional — the
|
||||||
|
* controller fills sensible defaults so an empty `{}` publishes a `payment.succeeded` to the
|
||||||
|
* passenger queue. Set `referenceId` to a real bookingId to exercise the consumer's side effects
|
||||||
|
* (seat confirm / ticket issue); leave it blank to only prove RabbitMQ delivery.
|
||||||
|
*/
|
||||||
|
export class TestPaymentEventDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: ["payment.succeeded", "payment.failed"],
|
||||||
|
default: "payment.succeeded",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(["payment.succeeded", "payment.failed"])
|
||||||
|
eventType?: PaymentEventType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: PaymentService, default: PaymentService.PASSENGER })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(PaymentService)
|
||||||
|
service?: PaymentService;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: PaymentReferenceType,
|
||||||
|
default: PaymentReferenceType.BOOKING,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(PaymentReferenceType)
|
||||||
|
referenceType?: PaymentReferenceType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Domain order id (e.g. bookingId). Defaults to a random uuid.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
referenceId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Defaults to a random uuid." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
intentId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Defaults to test-<random>." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
merchantOrderId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ProviderMethod, default: ProviderMethod.WAAFI })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ProviderMethod)
|
||||||
|
provider?: ProviderMethod;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 10000, description: "Amount in minor units." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
amountMinor?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: "ETB" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Only used for payment.succeeded." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
providerTxnId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Only used for payment.failed." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
failureCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Only used for payment.failed." })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
failureMessage?: string;
|
||||||
|
}
|
||||||
@@ -11,6 +11,12 @@ import { OutboxRepository } from "./outbox.repository";
|
|||||||
import { HttpPaymentEventPublisher } from "./publisher/http-payment-event-publisher";
|
import { HttpPaymentEventPublisher } from "./publisher/http-payment-event-publisher";
|
||||||
import { PAYMENT_EVENT_PUBLISHER } from "./publisher/payment-event-publisher";
|
import { PAYMENT_EVENT_PUBLISHER } from "./publisher/payment-event-publisher";
|
||||||
import { RabbitMqPaymentEventPublisher } from "./publisher/rabbitmq-payment-event-publisher";
|
import { RabbitMqPaymentEventPublisher } from "./publisher/rabbitmq-payment-event-publisher";
|
||||||
|
import { TestEventsController } from "./test-events.controller";
|
||||||
|
|
||||||
|
// Dev-only harness to publish a synthetic payment event straight to the broker.
|
||||||
|
// Never registered in production, so the endpoint cannot exist there.
|
||||||
|
const testControllers =
|
||||||
|
process.env.NODE_ENV !== "production" ? [TestEventsController] : [];
|
||||||
|
|
||||||
const rabbitImports = isRabbitPublisher()
|
const rabbitImports = isRabbitPublisher()
|
||||||
? [
|
? [
|
||||||
@@ -42,6 +48,7 @@ const rabbitImports = isRabbitPublisher()
|
|||||||
HttpModule,
|
HttpModule,
|
||||||
...rabbitImports,
|
...rabbitImports,
|
||||||
],
|
],
|
||||||
|
controllers: testControllers,
|
||||||
providers: [
|
providers: [
|
||||||
OutboxRepository,
|
OutboxRepository,
|
||||||
OutboxRelayService,
|
OutboxRelayService,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { Body, Controller, Inject, Logger, Post } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
PaymentEvent,
|
||||||
|
PaymentReferenceType,
|
||||||
|
PaymentService,
|
||||||
|
ProviderMethod,
|
||||||
|
paymentRoutingKey,
|
||||||
|
} from "@edr/types";
|
||||||
|
import {
|
||||||
|
PAYMENT_EVENT_PUBLISHER,
|
||||||
|
PaymentEventPublisher,
|
||||||
|
} from "./publisher/payment-event-publisher";
|
||||||
|
import { TestPaymentEventDto } from "./dto/test-payment-event.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEV-ONLY test harness. Publishes a synthetic payment event through the real
|
||||||
|
* PaymentEventPublisher (RabbitMQ in dev), so the passenger/freight consumer receives it
|
||||||
|
* exactly as in production — without creating an intent or going through a booking + provider
|
||||||
|
* flow. Registered only when NODE_ENV !== "production" (see OutboxModule); never reachable in prod.
|
||||||
|
*
|
||||||
|
* Quick check (no body): POST /test/payment-event -> publishes payment.passenger.succeeded.
|
||||||
|
* Real side effects: pass a real bookingId as `referenceId`.
|
||||||
|
*/
|
||||||
|
@ApiTags("Dev test (non-production)")
|
||||||
|
@Controller("test")
|
||||||
|
export class TestEventsController {
|
||||||
|
private readonly logger = new Logger(TestEventsController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PAYMENT_EVENT_PUBLISHER)
|
||||||
|
private readonly publisher: PaymentEventPublisher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post("payment-event")
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"DEV ONLY: publish a synthetic payment event to the broker (passenger/freight consumes it)",
|
||||||
|
description:
|
||||||
|
"Bypasses intents/booking. Empty body publishes a payment.succeeded for PASSENGER. " +
|
||||||
|
"Set referenceId to a real bookingId to trigger the consumer's seat/ticket side effects.",
|
||||||
|
})
|
||||||
|
async publishTestEvent(
|
||||||
|
@Body() dto: TestPaymentEventDto,
|
||||||
|
): Promise<{ published: true; routingKey: string; event: PaymentEvent }> {
|
||||||
|
const eventType = dto.eventType ?? "payment.succeeded";
|
||||||
|
const service = dto.service ?? PaymentService.PASSENGER;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
version: 1 as const,
|
||||||
|
eventId: randomUUID(),
|
||||||
|
occurredAt: now,
|
||||||
|
service,
|
||||||
|
intentId: dto.intentId ?? randomUUID(),
|
||||||
|
referenceType: dto.referenceType ?? PaymentReferenceType.BOOKING,
|
||||||
|
referenceId: dto.referenceId ?? randomUUID(),
|
||||||
|
merchantOrderId: dto.merchantOrderId ?? `test-${randomUUID().slice(0, 8)}`,
|
||||||
|
provider: dto.provider ?? ProviderMethod.WAAFI,
|
||||||
|
amountMinor: dto.amountMinor ?? 10_000,
|
||||||
|
currency: dto.currency ?? "ETB",
|
||||||
|
};
|
||||||
|
|
||||||
|
const event: PaymentEvent =
|
||||||
|
eventType === "payment.failed"
|
||||||
|
? {
|
||||||
|
...base,
|
||||||
|
eventType: "payment.failed",
|
||||||
|
failureCode: dto.failureCode ?? "TEST_DECLINED",
|
||||||
|
failureMessage: dto.failureMessage ?? "Synthetic test failure",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...base,
|
||||||
|
eventType: "payment.succeeded",
|
||||||
|
providerTxnId: dto.providerTxnId ?? `TEST-${randomUUID().slice(0, 8)}`,
|
||||||
|
paidAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.publisher.publish(event);
|
||||||
|
|
||||||
|
const routingKey = paymentRoutingKey(event.service, event.eventType);
|
||||||
|
this.logger.log(
|
||||||
|
`published TEST ${event.eventType} (${event.eventId}) ref=${event.referenceId} -> ${routingKey}`,
|
||||||
|
);
|
||||||
|
return { published: true, routingKey, event };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user