mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 10:10:57 +00:00
Merge branch 'alpha' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -20,7 +20,8 @@ export enum PaymentMethodTypeEnum {
|
||||
TELEBIRR = "TELEBIRR", // Ethiopia
|
||||
CBE_BIRR = "CBE_BIRR", // Ethiopia
|
||||
EBIRR = "EBIRR", // Ethiopia
|
||||
WAAFI = "WAAFI", // Djibouti
|
||||
WAAFI = "WAAFI",
|
||||
DMONEY= "DMONEY",// Djibouti
|
||||
CARD = "CARD", // International
|
||||
WALLET = "WALLET", // Internal
|
||||
}
|
||||
|
||||
@@ -1,64 +1,23 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";
|
||||
import {
|
||||
PAYMENT_EVENTS_DLX,
|
||||
PAYMENT_EVENTS_EXCHANGE,
|
||||
PAYMENT_QUEUES,
|
||||
PaymentService,
|
||||
paymentServiceBindingPattern,
|
||||
} from "@edr/types";
|
||||
import { PaymentsController } from "./payments.controller";
|
||||
import { PaymentsService } from "./payments.service";
|
||||
import { InternalPaymentsController } from "./internal-payments.controller";
|
||||
import { PaymentClientService } from "./payment-client.service";
|
||||
import { PaymentEventsConsumer } from "./payment-events.consumer";
|
||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||
import { SeatsModule } from "../seats/seats.module";
|
||||
import { TicketsModule } from "../tickets/tickets.module";
|
||||
|
||||
const PASSENGER_QUEUE = PAYMENT_QUEUES[PaymentService.PASSENGER];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SeatsModule,
|
||||
TicketsModule,
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
...(process.env.PAYMENT_RABBITMQ_URL
|
||||
? [
|
||||
RabbitMQModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
uri: config.get<string>("rabbitmq.url") as string,
|
||||
exchanges: [
|
||||
{
|
||||
name: PAYMENT_EVENTS_EXCHANGE,
|
||||
type: "topic",
|
||||
options: { durable: true },
|
||||
},
|
||||
{ name: PAYMENT_EVENTS_DLX, type: "topic", options: { durable: true } },
|
||||
],
|
||||
queues: [
|
||||
{
|
||||
name: PASSENGER_QUEUE.dlq,
|
||||
exchange: PAYMENT_EVENTS_DLX,
|
||||
routingKey: paymentServiceBindingPattern(PaymentService.PASSENGER),
|
||||
options: { durable: true },
|
||||
},
|
||||
],
|
||||
prefetchCount: config.get<number>("rabbitmq.prefetch") ?? 10,
|
||||
connectionInitOptions: { wait: false },
|
||||
}),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
controllers: [PaymentsController, InternalPaymentsController],
|
||||
providers: [
|
||||
PaymentsService,
|
||||
PaymentClientService,
|
||||
PaymentEventsConsumer,
|
||||
ServiceAuthGuard,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -43,6 +43,14 @@ const NON_TERMINAL_STATUSES: PaymentIntentStatus[] = [
|
||||
export class PaymentsService {
|
||||
private readonly logger = new Logger(PaymentsService.name);
|
||||
|
||||
/**
|
||||
* DEMO ONLY: when true, a WALLET "payment" is treated as instantly successful — the wallet
|
||||
* balance check and debit are skipped and the booking is confirmed + ticket issued as if fully
|
||||
* paid. Lets the happy-path be demoed while a real provider (e.g. Telebirr) is unavailable.
|
||||
* Never enable in production. Toggle with WALLET_DEMO_AUTO_SUCCEED in the env.
|
||||
*/
|
||||
private readonly walletDemoAutoSucceed = true;
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private seatsService: SeatsService,
|
||||
@@ -196,6 +204,35 @@ export class PaymentsService {
|
||||
private async initiateWalletPayment(
|
||||
booking: Prisma.BookingGetPayload<{ include: { seats: true } }>,
|
||||
): Promise<InitiateResponseDto> {
|
||||
// DEMO ONLY (WALLET_DEMO_AUTO_SUCCEED): pretend the payment succeeded — no balance check,
|
||||
// no debit — and run the exact same finalize path a real successful payment uses
|
||||
// (booking → CONFIRMED, seats confirmed, ticket issued). Remove once a real provider works.
|
||||
if (this.walletDemoAutoSucceed) {
|
||||
this.logger.warn(
|
||||
`WALLET_DEMO_AUTO_SUCCEED enabled — faking a successful WALLET payment for booking ${booking.bookingRef} (${booking.id})`,
|
||||
);
|
||||
const demoIntent = await this.prisma.paymentIntent.upsert({
|
||||
where: { bookingId: booking.id },
|
||||
update: {
|
||||
status: PaymentIntentStatus.PROCESSING,
|
||||
failureCode: null,
|
||||
method: PaymentMethodType.WALLET,
|
||||
},
|
||||
create: {
|
||||
bookingId: booking.id,
|
||||
amountMinor: booking.totalMinor,
|
||||
method: PaymentMethodType.WALLET,
|
||||
status: PaymentIntentStatus.PROCESSING,
|
||||
providerRef: `WALLET-DEMO-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
await this.finalizePaymentSuccess({ intentId: demoIntent.id });
|
||||
const settled = await this.prisma.paymentIntent.findUniqueOrThrow({
|
||||
where: { id: demoIntent.id },
|
||||
});
|
||||
return this.formatIntentResponse(settled);
|
||||
}
|
||||
|
||||
const debitResult = await this.prisma.$transaction(async (tx) => {
|
||||
const wallet = await tx.walletAccount.findUnique({
|
||||
where: { passengerId: booking.passengerId },
|
||||
|
||||
@@ -44,6 +44,17 @@ export class TicketsController {
|
||||
});
|
||||
}
|
||||
|
||||
@Get('by-order/:merchantOrderId')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Get ticket by merchant order ID',
|
||||
description: 'Looks up the booking ID from the PaymentIntent using merchantOrderId, then returns the full ticket information.'
|
||||
})
|
||||
getByMerchantOrderId(@Param('merchantOrderId') merchantOrderId: string) {
|
||||
return this.service.getByMerchantOrderId(merchantOrderId);
|
||||
}
|
||||
|
||||
@Get(':bookingRef')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
|
||||
@@ -161,6 +161,28 @@ export class TicketsService {
|
||||
return { success: true, updatedSeats: newSeatIds.length };
|
||||
}
|
||||
|
||||
async getByMerchantOrderId(merchantOrderId: string) {
|
||||
const intent = await this.prisma.paymentIntent.findUnique({
|
||||
where: { merchantOrderId },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
if (!intent) throw new NotFoundException(`No payment intent found for order ${merchantOrderId}`);
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: intent.bookingId },
|
||||
include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, seats: { include: { seat: { include: { coach: true } } } }, ticket: true },
|
||||
});
|
||||
if (!booking?.ticket) throw new NotFoundException('Ticket not found');
|
||||
const seat = booking.seats[0];
|
||||
return {
|
||||
id: booking.ticket.id, bookingId: booking.id, bookingRef: booking.bookingRef, status: booking.status,
|
||||
fromStationName: booking.schedule.originStation.name, toStationName: booking.schedule.destinationStation.name,
|
||||
departureAt: booking.schedule.departureAt, trainName: booking.schedule.train.name,
|
||||
coachLabel: seat?.seat.coach.number, seatLabel: seat?.seat.seatNumber, passengerName: seat?.passengerName,
|
||||
priceMinor: booking.totalMinor, currency: booking.currency, qrPayload: booking.ticket.qrPayload,
|
||||
barcodePayload: booking.ticket.barcodePayload,
|
||||
};
|
||||
}
|
||||
|
||||
async getByRef(bookingRef: string) {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { bookingRef },
|
||||
|
||||
Reference in New Issue
Block a user