import { Injectable, Logger } 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]; @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 { // 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); } } }