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 }; } }