Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha

This commit is contained in:
Stephanos A
2026-07-16 19:28:12 +03:00
29 changed files with 809 additions and 103 deletions

View File

@@ -1,4 +1,5 @@
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 {
@@ -18,7 +19,15 @@ const PASSENGER_QUEUE = PAYMENT_QUEUES[PaymentService.PASSENGER];
export class PaymentEventsConsumer {
private readonly logger = new Logger(PaymentEventsConsumer.name);
constructor(private readonly paymentsService: PaymentsService) {}
// 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({
@@ -37,7 +46,13 @@ export class PaymentEventsConsumer {
`RECEIVED ${event.eventType} (${event.eventId}) ref=${event.referenceId} via RabbitMQ`,
);
try {
const result = await this.paymentsService.handlePaymentEvent(
// 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(

View File

@@ -154,6 +154,13 @@ export class IntentStatusDto {
@ApiPropertyOptional() paidAt?: string;
@ApiPropertyOptional() failureCode?: string;
@ApiPropertyOptional() failureMessage?: string;
@ApiPropertyOptional({
type: "object",
additionalProperties: true,
description:
"Raw provider payload (initiation response merged with the latest status query) for inspection/debugging. Provider-specific shape; never trusted for state.",
})
providerResponse?: Record<string, unknown>;
}
export class BookingAmountResponseDto {

View File

@@ -432,6 +432,9 @@ export class PaymentsService {
expiresAt: snapshot.expiresAt ? new Date(snapshot.expiresAt) : null,
failureCode: snapshot.failureCode ?? null,
failureMessage: snapshot.failureMessage ?? null,
rawInitiation: snapshot.providerResponse
? (snapshot.providerResponse as unknown as Prisma.InputJsonValue)
: Prisma.DbNull,
};
return this.prisma.paymentIntent.upsert({
where: { bookingId },
@@ -589,6 +592,10 @@ export class PaymentsService {
paidAt: intent.paidAt?.toISOString(),
failureCode: intent.failureCode ?? undefined,
failureMessage: intent.failureMessage ?? undefined,
providerResponse:
intent.rawInitiation && typeof intent.rawInitiation === "object"
? (intent.rawInitiation as Record<string, unknown>)
: undefined,
};
}