mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Prisma, PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
|
import {
|
|
CardProvider,
|
|
CardWebhookPayload,
|
|
ProviderPaymentStatus,
|
|
} from '@edr/payment-providers';
|
|
import { PrismaService } from '../../../common/prisma.service';
|
|
import { PaymentsService } from '../payments.service';
|
|
|
|
@Injectable()
|
|
export class CardWebhookService {
|
|
private readonly logger = new Logger(CardWebhookService.name);
|
|
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly provider: CardProvider,
|
|
private readonly payments: PaymentsService,
|
|
) {}
|
|
|
|
async handle(payload: CardWebhookPayload, signature: string): Promise<void> {
|
|
const merchantOrderId = payload.data.object.metadata.merchantOrderId;
|
|
const externalEventId = `${payload.id}_${payload.type}`;
|
|
const signatureValid = this.provider.verifyWebhookSignature(
|
|
payload as unknown as Record<string, unknown>,
|
|
signature,
|
|
);
|
|
|
|
const eventRow = await this.persistEvent({
|
|
externalEventId,
|
|
merchantOrderId,
|
|
providerTxnId: payload.data.object.transaction_id,
|
|
signatureValid,
|
|
status: payload.data.object.status,
|
|
payload,
|
|
});
|
|
|
|
if (!eventRow) {
|
|
this.logger.log(`Card webhook duplicate: ${externalEventId} — short-circuit OK`);
|
|
return;
|
|
}
|
|
|
|
if (!signatureValid) {
|
|
this.logger.warn(`Card webhook signature invalid for merchantOrderId=${merchantOrderId}`);
|
|
await this.markProcessed(eventRow.id, 'signature-invalid');
|
|
return;
|
|
}
|
|
|
|
const intent = await this.prisma.paymentIntent.findUnique({
|
|
where: { merchantOrderId },
|
|
});
|
|
if (!intent) {
|
|
this.logger.warn(`Card webhook: no PaymentIntent for merchantOrderId=${merchantOrderId}`);
|
|
await this.markProcessed(eventRow.id, 'intent-not-found');
|
|
return;
|
|
}
|
|
|
|
const mapped = this.provider.mapWebhookStatus(payload.data.object.status);
|
|
|
|
try {
|
|
if (mapped === ProviderPaymentStatus.SUCCEEDED) {
|
|
await this.payments.finalizePaymentSuccess({
|
|
intentId: intent.id,
|
|
providerTxnId: payload.data.object.transaction_id,
|
|
paidAt: payload.data.object.paid_at ? new Date(payload.data.object.paid_at * 1000) : undefined,
|
|
});
|
|
} else if (mapped === ProviderPaymentStatus.FAILED) {
|
|
await this.payments.markPaymentFailed({
|
|
intentId: intent.id,
|
|
failureCode: payload.data.object.failure_code,
|
|
failureMessage: payload.data.object.failure_message,
|
|
});
|
|
} else {
|
|
await this.prisma.paymentIntent.update({
|
|
where: { id: intent.id },
|
|
data: {
|
|
status: mapped as unknown as PaymentIntentStatus,
|
|
providerTxnId: payload.data.object.transaction_id ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
await this.markProcessed(eventRow.id);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
this.logger.error(`Card 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: CardWebhookPayload;
|
|
}): Promise<{ id: string } | null> {
|
|
try {
|
|
return await this.prisma.paymentWebhookEvent.create({
|
|
data: {
|
|
provider: PaymentMethodType.CARD,
|
|
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 },
|
|
});
|
|
}
|
|
}
|