import { BadRequestException, forwardRef, Inject, Injectable, InternalServerErrorException, NotFoundException, } from "@nestjs/common"; import { DataSource } from "typeorm"; import { PaymentEntity } from "./entities/payment.entity"; import { PaymentRepository } from "./payment.repository"; import * as fs from "fs"; import * as path from "path"; import * as Handlebars from "handlebars"; import { ConfigService } from "@nestjs/config"; // import { SchedulingStatus } from "@edr/types"; import { Booking } from "../bookings/entities/booking.entity"; import { ClientAction, createMerchantOrderId, ProviderPaymentStatus, TelebirrProvider, } from "@edr/payment-providers"; import { ProviderInitiationInput } from "@edr/types" import { InitiateResponseDto, PaymentPlatformDto } from "./payments.dto"; import { BookingBatchService } from "../train-scheduling/booking-batch.service"; const DEFAULT_CURRENCY = "ETB"; @Injectable() export class PaymentService { constructor( private readonly configService: ConfigService, private readonly datasource: DataSource, private readonly paymentRepo: PaymentRepository, private readonly telebirrProvider: TelebirrProvider, @Inject(forwardRef(() => BookingBatchService)) private readonly bookingBatchService: BookingBatchService, ) { } async initBookingTelebirr( bookingId: string, platform: PaymentPlatformDto, ): Promise<{ redirectUrl: string }> { // const booking = await this.datasource.getRepository(Booking).findOneBy({ id: bookingId }); // if (!booking) throw new NotFoundException("Booking not found"); // const booking = new Booking() // booking.totalAmount = 20 // booking.id = randomUUID const amount = 20 const merchantOrderId = createMerchantOrderId(); const redirectBase = this.configService.get("TELEBIRR_SUCCESS_BOOKING_REDIRECT_BASE_URL"); const redirectUrl = `${redirectBase}/${merchantOrderId}`; const amountMinor = Math.round(Number(amount) * 100); const input: ProviderInitiationInput = { merchantOrderId, orderRef: bookingId, amountMinor, currency: DEFAULT_CURRENCY, platform: platform || "web", redirectUrl, }; const result = await this.telebirrProvider.initiate(input); const payment = await this.paymentRepo.create({ amount: amount, currency: DEFAULT_CURRENCY, method: "telebirr", refId: bookingId, type: "booking", merchantOrderId, rawInitiation: result.rawInitiation, clientAction: result.clientAction as Record, expiresAt: result.expiresAt, reason: `Payment for booking`, }); return { redirectUrl: `${this.configService.get("TELEBIRR_REDIRECT_BASE_URL")}/${payment.merchantOrderId}` } } async getActivePaymentByOrderIdAndMethod(orderId: string, method: PaymentEntity["method"]): Promise { return this.paymentRepo.getActivePaymentByOrderIdAndMethod(orderId, method) } async genReceiptHtml(orderId: string) { const payment = await this.paymentRepo.findOneBy({ merchantOrderId: orderId, status: "success" }) if (!payment) { throw new BadRequestException() } const filePath = path.join(__dirname, "templates", "receipt.hbs"); if (!fs.existsSync(filePath)) { throw new InternalServerErrorException() } const source = fs.readFileSync(filePath, "utf8"); const template = Handlebars.compile(source); const html = template({ vendorName: "Ethio Djibouti Railway Ticket Booking", vendorAddress: "Addis Ababa", receiptDate: payment.paidAt, paymentMethod: payment?.method, subtotal: payment?.amount.toString(), total: payment?.amount.toString(), currency: payment?.currency, reason: payment?.reason }); return html; } async checkStatusAndUpdate(orderId: string) { const resp = await this.paymentRepo.findOneBy({ merchantOrderId: orderId }) if (!resp) { throw new NotFoundException("order id not found") } const result = await this.telebirrProvider.queryStatus(resp.merchantOrderId) if (result.status === ProviderPaymentStatus.SUCCEEDED) { await this.datasource.transaction(async (mg) => { await mg.update(PaymentEntity, { id: resp.id }, { status: "success" }) await mg.update(Booking, { id: resp.refId }, { paymentStatus: "PAID" }) }) if (resp.type === "booking") { await this.bookingBatchService.ensurePaidBookingAllocated(resp.refId) } } return { status: result.status } } findBookingById(id: string) { return this.paymentRepo.findOneBy({ refId: id, type: "booking" }) } formatIntentResponse(intent: PaymentEntity): InitiateResponseDto { const clientAction = intent.clientAction && typeof intent.clientAction === "object" ? (intent.clientAction as unknown as ClientAction) : undefined; const statusMap: Record = { "action-required": ProviderPaymentStatus.REQUIRES_ACTION, "processing": ProviderPaymentStatus.PROCESSING, "success": ProviderPaymentStatus.SUCCEEDED, "failed": ProviderPaymentStatus.FAILED, "canceled": ProviderPaymentStatus.CANCELLED, "refunded": ProviderPaymentStatus.CANCELLED, }; return { intentId: intent.id, status: statusMap[intent.status] ?? ProviderPaymentStatus.PROCESSING, clientAction, merchantOrderId: intent.merchantOrderId ?? undefined, }; } }