mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
75 lines
3.3 KiB
TypeScript
75 lines
3.3 KiB
TypeScript
import { Injectable, Logger, SetMetadata } from '@nestjs/common';
|
|
import { ModuleRef } from '@nestjs/core';
|
|
import { Nack, RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
|
|
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
|
import {
|
|
PAYMENT_EVENTS_DLX,
|
|
PAYMENT_EVENTS_EXCHANGE,
|
|
PAYMENT_QUEUES,
|
|
PaymentEvent,
|
|
PaymentService,
|
|
paymentServiceBindingPattern,
|
|
} from '@edr/types';
|
|
import { PaymentEventDto } from './internal-payments.dto';
|
|
import { PaymentsService } from './payments.service';
|
|
|
|
const PASSENGER_QUEUE = PAYMENT_QUEUES[PaymentService.PASSENGER];
|
|
|
|
// @tria-plc/auditlog's global ClientLoggerInterceptor (present in deployed builds)
|
|
// crashes on non-HTTP contexts (`originalUrl.split` on a RabbitMQ message) and the
|
|
// resulting requeue storm blocks payment.succeeded forever. Its IgnoreLoggerAudit
|
|
// decorator is just this metadata key — set it directly so we don't need the package.
|
|
@SetMetadata('ignoreAuditLogger', true)
|
|
@Injectable()
|
|
export class PaymentEventsConsumer {
|
|
private readonly logger = new Logger(PaymentEventsConsumer.name);
|
|
|
|
// IMPORTANT: do NOT constructor-inject PaymentsService here. It is a REQUEST/TRANSIENT-scoped
|
|
// provider (its scope bubbles up from a scoped dependency), so it has no singleton instance at
|
|
// bootstrap. Constructor-injecting it makes THIS consumer scoped too — and golevelup binds the
|
|
// @RabbitSubscribe handler to the singleton instance it discovers at bootstrap. With no such
|
|
// instance, the subscription still registers but delivered messages are never dispatched to
|
|
// handle(): they pile up unacked and the booking never confirms. Injecting only the lightweight
|
|
// (singleton) ModuleRef keeps this consumer a clean singleton; PaymentsService is resolved per
|
|
// message via resolve() (get() throws for scoped providers).
|
|
constructor(private readonly moduleRef: ModuleRef) {}
|
|
|
|
@IsPublic()
|
|
@RabbitSubscribe({
|
|
exchange: PAYMENT_EVENTS_EXCHANGE,
|
|
routingKey: paymentServiceBindingPattern(PaymentService.PASSENGER), // payment.passenger.*
|
|
queue: PASSENGER_QUEUE.main,
|
|
queueOptions: {
|
|
durable: true,
|
|
deadLetterExchange: PAYMENT_EVENTS_DLX,
|
|
},
|
|
})
|
|
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 {
|
|
// resolve() (not get()) because PaymentsService is scoped — get() throws for scoped providers.
|
|
const paymentsService = await this.moduleRef.resolve(
|
|
PaymentsService,
|
|
undefined,
|
|
{ strict: false },
|
|
);
|
|
const result = await paymentsService.handlePaymentEvent(
|
|
event as unknown as PaymentEventDto,
|
|
);
|
|
this.logger.log(
|
|
`processed ${event.eventType} (${event.eventId}) ref=${event.referenceId}: ${JSON.stringify(result)}`,
|
|
);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
this.logger.error(
|
|
`DEAD-LETTERING ${event.eventType} (${event.eventId}) ref=${event.referenceId}: ${message}`,
|
|
);
|
|
return new Nack(false);
|
|
}
|
|
}
|
|
}
|