import { Injectable, Logger } from '@nestjs/common'; import { Prisma, PaymentIntentStatus, PaymentMethodType } from '@prisma/client'; import { EBirrProvider, EBirrWebhookPayload, ProviderPaymentStatus, } from '@edr/payment-providers'; import { PrismaService } from '../../../common/prisma.service'; import { PaymentsService } from '../payments.service'; @Injectable() export class EBirrWebhookService { private readonly logger = new Logger(EBirrWebhookService.name); constructor( private readonly prisma: PrismaService, private readonly provider: EBirrProvider, private readonly payments: PaymentsService, ) {} async handle(payload: EBirrWebhookPayload): Promise { const merchantOrderId = payload.orderNo; const externalEventId = `${payload.orderNo}_${payload.tradeStatus}_${payload.timestamp}`; const signatureValid = this.provider.verifyWebhookSignature( payload as unknown as Record, ); const eventRow = await this.persistEvent({ externalEventId, merchantOrderId, providerTxnId: payload.tradeNo, signatureValid, status: payload.tradeStatus, payload, }); if (!eventRow) { this.logger.log(`eBirr webhook duplicate: ${externalEventId} — short-circuit OK`); return; } if (!signatureValid) { this.logger.warn(`eBirr webhook signature invalid for orderNo=${merchantOrderId}`); await this.markProcessed(eventRow.id, 'signature-invalid'); return; } const intent = await this.prisma.paymentIntent.findUnique({ where: { merchantOrderId }, }); if (!intent) { this.logger.warn(`eBirr webhook: no PaymentIntent for orderNo=${merchantOrderId}`); await this.markProcessed(eventRow.id, 'intent-not-found'); return; } const mapped = this.provider.mapWebhookStatus(payload.tradeStatus); try { if (mapped === ProviderPaymentStatus.SUCCEEDED) { await this.payments.finalizePaymentSuccess({ intentId: intent.id, providerTxnId: payload.tradeNo, paidAt: payload.payTime ? new Date(payload.payTime) : undefined, }); } else if (mapped === ProviderPaymentStatus.FAILED) { await this.payments.markPaymentFailed({ intentId: intent.id, failureCode: payload.tradeStatus, }); } else { await this.prisma.paymentIntent.update({ where: { id: intent.id }, data: { status: mapped as unknown as PaymentIntentStatus, providerTxnId: payload.tradeNo ?? undefined, }, }); } await this.markProcessed(eventRow.id); } catch (err) { const message = err instanceof Error ? err.message : String(err); this.logger.error(`eBirr webhook processing failed for ${merchantOrderId}: ${message}`); await this.markProcessed(eventRow.id, `processing-error: ${message}`); throw err; } } private async persistEvent(input: { externalEventId: string; merchantOrderId: string; providerTxnId?: string; signatureValid: boolean; status: string; payload: EBirrWebhookPayload; }): Promise<{ id: string } | null> { try { return await this.prisma.paymentWebhookEvent.create({ data: { provider: PaymentMethodType.EBIRR, externalEventId: input.externalEventId, merchantOrderId: input.merchantOrderId, providerTxnId: input.providerTxnId, signatureValid: input.signatureValid, status: input.status, payload: input.payload as unknown as Prisma.InputJsonValue, }, select: { id: true }, }); } catch (err) { if ( err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002' ) { return null; } throw err; } } private async markProcessed(eventId: string, processingError?: string): Promise { await this.prisma.paymentWebhookEvent.update({ where: { id: eventId }, data: { processedAt: new Date(), processingError }, }); } }