mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 01:20:55 +00:00
128 lines
4.0 KiB
TypeScript
128 lines
4.0 KiB
TypeScript
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<void> {
|
|
const merchantOrderId = payload.orderNo;
|
|
const externalEventId = `${payload.orderNo}_${payload.tradeStatus}_${payload.timestamp}`;
|
|
const signatureValid = this.provider.verifyWebhookSignature(
|
|
payload as unknown as Record<string, unknown>,
|
|
);
|
|
|
|
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<void> {
|
|
await this.prisma.paymentWebhookEvent.update({
|
|
where: { id: eventId },
|
|
data: { processedAt: new Date(), processingError },
|
|
});
|
|
}
|
|
}
|