import { Injectable, NotFoundException } from "@nestjs/common"; import { DataSource, QueryRunner } from "typeorm"; import { PaymentEntity } from "./entities/payment.entity"; import { PaymentStrategy } from "./strategies/payment.strategy"; import { PaymentTelebirrStrategy } from "./strategies/payment.telebirr.strategy"; import { PaymentRepository } from "./payment.repository"; import { ClientAction, PaymentPlatform } from "./strategies/payments.types"; import * as crypto from 'crypto'; import { PaymentStatus, UpdatePaymentStatusDto } from "./dto/update-payment-status.dto"; type PaymentMethod = PaymentEntity["method"] type CurrencyType = PaymentEntity["currency"] @Injectable() export class PaymentService { private strategies: Map; constructor( private readonly datasource: DataSource, private readonly paymentRepo: PaymentRepository, private readonly telebirrPaymentStategy: PaymentTelebirrStrategy) { this.strategies = new Map([ ["telebirr", this.telebirrPaymentStategy as PaymentStrategy] ]) } async pay(amount: number, currency: CurrencyType, method: PaymentMethod, cb: (qr: QueryRunner) => Promise<{ id: string, type: PaymentEntity["type"] }>, payform: PaymentPlatform = "web"): Promise<{ refId: string, clientAction: ClientAction, status: PaymentEntity["status"], paidAt?: string, failureCode?: string, failureMessage?: string, }> { const strategy = this.strategies.get(method) if (!strategy) { throw new NotFoundException("strategy not found") } const orderId = `freigh${Date.now()}${crypto.randomBytes(4).toString('hex')}` //todo: make it dynamic const paymentResp = await strategy.pay({ amountMinor: amount, currency: currency, merchantOrderId: orderId, platform: payform, }); const queryRunner = this.datasource.createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction() console.log(paymentResp.expiresAt) try { const resp = await cb(queryRunner) const payment = await this.paymentRepo.createTr(queryRunner, { amount, currency, method, refId: resp.id, type: resp.type, merchantOrderId: orderId, rawInitiation: paymentResp.rawInitiation, clientAction: paymentResp.clientAction, expiresAt: paymentResp.expiresAt }) await queryRunner.commitTransaction() return { refId: payment.refId, clientAction: paymentResp.clientAction, status: payment.status, paidAt: payment.paidAt?.toISOString(), failureCode: payment.failerCode ?? undefined, failureMessage: payment.failureMessage ?? undefined, } } catch (err) { await queryRunner.rollbackTransaction() throw new Error("payment failed") } finally { await queryRunner.release() } } async getActivePaymentByRefIdAndMethod(refId: string, method: PaymentEntity["method"]): Promise { return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method) } async handleTelebirrPaymentCb(dto: UpdatePaymentStatusDto): Promise { switch (dto.status) { case PaymentStatus.SUCCEEDED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "success" }) break; case PaymentStatus.FAILED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "failed", failureMessage: dto.failureMessage }) break; case PaymentStatus.CANCELLED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) break; case PaymentStatus.PROCESSING: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) break; case PaymentStatus.REFUNDED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) break; } } }