Initial commit of edr-passenger-api alpha version

This commit is contained in:
Stephanos A
2026-05-13 16:58:49 +03:00
parent 199a3eba11
commit 39ba561d8f
113 changed files with 3602 additions and 1035 deletions

View File

@@ -1,21 +1,81 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Payment } from "./entities/payment.entity";
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { SeatsService } from '../seats/seats.service';
import { TicketsService } from '../tickets/tickets.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto } from './payments.dto';
import { telebirrAdapter, cbeBirrAdapter, eBirrAdapter, cardAdapter } from './payments.adapters';
@Injectable()
export class PaymentsService {
constructor(
@InjectRepository(Payment)
private readonly paymentsRepository: Repository<Payment>,
private prisma: PrismaService,
private seatsService: SeatsService,
private ticketsService: TicketsService,
private eventEmitter: EventEmitter2,
) {}
/** List payments associated with a ticket. */
findByTicket(ticketId: string): Promise<Payment[]> {
return this.paymentsRepository.find({
where: { ticketId },
order: { createdAt: "DESC" },
async initiatePayment(dto: InitiatePaymentDto) {
const booking = await this.prisma.booking.findUnique({ where: { id: dto.bookingId }, include: { seats: true } });
if (!booking) throw new NotFoundException('Booking not found');
if (booking.status !== 'PENDING_PAYMENT') throw new BadRequestException('Booking not payable');
let result;
if (dto.method === 'WALLET') {
result = await this.prisma.$transaction(async (tx) => {
const wallet = await tx.walletAccount.findUnique({ where: { passengerId: booking.passengerId } });
if (!wallet || wallet.balanceMinor < booking.totalMinor) return { success: false, providerRef: '' };
const newBalance = wallet.balanceMinor - booking.totalMinor;
await tx.walletAccount.update({ where: { passengerId: booking.passengerId }, data: { balanceMinor: newBalance } });
await tx.walletLedgerEntry.create({ data: { walletId: wallet.id, type: 'DEBIT', amountMinor: booking.totalMinor, balanceAfterMinor: newBalance, description: `Train Ticket - ${booking.bookingRef}`, relatedBookingId: booking.id } });
return { success: true, providerRef: `WALLET-${Date.now()}` };
});
} else {
const adapters = { TELEBIRR: telebirrAdapter, CBE_BIRR: cbeBirrAdapter, EBIRR: eBirrAdapter, CARD: cardAdapter } as any;
result = await adapters[dto.method](booking.totalMinor, booking.bookingRef);
}
const status = result.success ? 'SUCCEEDED' : 'FAILED';
const intent = await this.prisma.paymentIntent.upsert({
where: { bookingId: dto.bookingId },
update: { status, providerRef: result.providerRef, clientAction: result.clientAction as any },
create: { bookingId: dto.bookingId, amountMinor: booking.totalMinor, method: dto.method as any, status: status as any, providerRef: result.providerRef, clientAction: result.clientAction as any },
});
if (result.success) {
await this.seatsService.confirmSeats(booking.seats.map((s) => s.seatId));
await this.prisma.booking.update({ where: { id: dto.bookingId }, data: { status: 'CONFIRMED' } });
await this.ticketsService.generate(dto.bookingId);
await this.awardLoyaltyPoints(booking.passengerId, booking.totalMinor, booking.id);
this.eventEmitter.emit('payment.succeeded', { booking });
}
return { id: intent.id, status: result.success ? 'SUCCESS' : 'FAILED', success: result.success };
}
async refund(dto: RefundDto) {
const intent = await this.prisma.paymentIntent.findUnique({ where: { bookingId: dto.bookingId } });
if (!intent || intent.status !== 'SUCCEEDED') throw new BadRequestException('No successful payment to refund');
await this.prisma.paymentIntent.update({ where: { bookingId: dto.bookingId }, data: { status: 'CANCELLED' } });
const booking = await this.prisma.booking.findUnique({ where: { id: dto.bookingId }, include: { seats: true } });
if (booking) {
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
await this.prisma.booking.update({ where: { id: dto.bookingId }, data: { status: 'CANCELLED' } });
}
return { refunded: true, bookingRef: booking?.bookingRef };
}
addPaymentMethod(dto: AddPaymentMethodDto) { return this.prisma.paymentMethod.create({ data: dto }); }
getPaymentMethods(userId: string) { return this.prisma.paymentMethod.findMany({ where: { userId }, orderBy: { isDefault: 'desc' } }); }
private async awardLoyaltyPoints(passengerId: string, amountMinor: number, bookingId: string) {
const points = Math.floor(amountMinor / 100);
const account = await this.prisma.loyaltyAccount.findUnique({ where: { passengerId } });
if (!account) return;
const newBalance = account.pointsBalance + points;
const tier = newBalance >= 10000 ? 'PLATINUM' : newBalance >= 5000 ? 'GOLD' : newBalance >= 2000 ? 'SILVER' : 'BRONZE';
await this.prisma.loyaltyAccount.update({ where: { passengerId }, data: { pointsBalance: { increment: points }, tier: tier as any } });
await this.prisma.loyaltyLedgerEntry.create({ data: { accountId: account.id, delta: points, reason: 'TRIP_COMPLETED', bookingId, balanceAfter: newBalance } });
}
}