mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -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 { PAYMENT_EVENT_PUBLISHER } from "./publisher/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()
|
||||
? [
|
||||
@@ -42,6 +48,7 @@ const rabbitImports = isRabbitPublisher()
|
||||
HttpModule,
|
||||
...rabbitImports,
|
||||
],
|
||||
controllers: testControllers,
|
||||
providers: [
|
||||
OutboxRepository,
|
||||
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