mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
Merge pull request #201 from Tria-plc/feat/payment-microservice
Feat/payment microservice
This commit is contained in:
@@ -79,6 +79,28 @@ export class PaymentsController {
|
|||||||
return this.service.getIntentByBookingId(bookingId);
|
return this.service.getIntentByBookingId(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("waafi/return")
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"DEMO ONLY — confirm a Waafi payment from the browser-return params and return JSON for the " +
|
||||||
|
"UI to display. The frontend success page forwards the Waafi query params here. Gated by " +
|
||||||
|
"WAAFI_DEMO_TRUST_RETURN (INSECURE; real confirmation is the webhook/HPP_GETTRANINFO).",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "referenceId", required: true })
|
||||||
|
@ApiQuery({ name: "state", required: true })
|
||||||
|
@ApiQuery({ name: "transactionId", required: false })
|
||||||
|
waafiReturn(
|
||||||
|
@Query("referenceId") referenceId: string,
|
||||||
|
@Query("state") state: string,
|
||||||
|
@Query("transactionId") transactionId: string,
|
||||||
|
) {
|
||||||
|
return this.service.confirmWaafiReturnDemo({
|
||||||
|
referenceId,
|
||||||
|
state,
|
||||||
|
transactionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Post("refund")
|
@Post("refund")
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
@UseGuards(JwtGuard, RolesGuard)
|
||||||
@Roles(UserRole.ADMIN, UserRole.STAFF, UserRole.AGENT)
|
@Roles(UserRole.ADMIN, UserRole.STAFF, UserRole.AGENT)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export class PaymentsService {
|
|||||||
private readonly logger = new Logger(PaymentsService.name);
|
private readonly logger = new Logger(PaymentsService.name);
|
||||||
private readonly walletDemoAutoSucceed = true;
|
private readonly walletDemoAutoSucceed = true;
|
||||||
|
|
||||||
|
private readonly waafiDemoTrustReturn = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private seatsService: SeatsService,
|
private seatsService: SeatsService,
|
||||||
@@ -192,6 +194,41 @@ export class PaymentsService {
|
|||||||
return { returnUrl, failureUrl };
|
return { returnUrl, failureUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async confirmWaafiReturnDemo(params: {
|
||||||
|
referenceId?: string;
|
||||||
|
state?: string;
|
||||||
|
transactionId?: string;
|
||||||
|
}): Promise<{ confirmed: boolean; bookingId?: string; reason?: string }> {
|
||||||
|
if (!this.waafiDemoTrustReturn) {
|
||||||
|
return { confirmed: false, reason: "demo-disabled" };
|
||||||
|
}
|
||||||
|
if ((params.state ?? "").toUpperCase() !== "APPROVED") {
|
||||||
|
return { confirmed: false, reason: `not-approved (${params.state})` };
|
||||||
|
}
|
||||||
|
if (!params.referenceId) {
|
||||||
|
return { confirmed: false, reason: "missing-referenceId" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const intent = await this.prisma.paymentIntent.findFirst({
|
||||||
|
where: { merchantOrderId: params.referenceId },
|
||||||
|
});
|
||||||
|
if (!intent) {
|
||||||
|
this.logger.warn(
|
||||||
|
`waafi demo return: no local intent for referenceId ${params.referenceId}`,
|
||||||
|
);
|
||||||
|
return { confirmed: false, reason: "intent-not-found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`WAAFI_DEMO_TRUST_RETURN enabled — confirming booking ${intent.bookingId} from browser return (INSECURE, demo only)`,
|
||||||
|
);
|
||||||
|
await this.finalizePaymentSuccess({
|
||||||
|
intentId: intent.id,
|
||||||
|
providerTxnId: params.transactionId,
|
||||||
|
});
|
||||||
|
return { confirmed: true, bookingId: intent.bookingId };
|
||||||
|
}
|
||||||
|
|
||||||
private async syncIntentProjection(
|
private async syncIntentProjection(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
snapshot: PaymentIntentSnapshot,
|
snapshot: PaymentIntentSnapshot,
|
||||||
@@ -453,6 +490,30 @@ export class PaymentsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard against an implausible paidAt from a provider event (e.g. a Telebirr epoch parsed as
|
||||||
|
* ms×1000 → year 58429), which Prisma/Postgres rejects and would otherwise dead-letter the
|
||||||
|
* whole confirmation. Falls back to "now" for missing/invalid/far-future/ancient values so the
|
||||||
|
* booking still confirms.
|
||||||
|
*/
|
||||||
|
private sanitizePaidAt(value?: Date): Date {
|
||||||
|
const now = new Date();
|
||||||
|
if (!value) return now;
|
||||||
|
const t = value.getTime();
|
||||||
|
const oneDayMs = 86_400_000;
|
||||||
|
if (
|
||||||
|
Number.isNaN(t) ||
|
||||||
|
t > now.getTime() + oneDayMs ||
|
||||||
|
t < Date.UTC(2000, 0, 1)
|
||||||
|
) {
|
||||||
|
this.logger.warn(
|
||||||
|
`finalizePaymentSuccess: implausible paidAt (epoch=${t}); using current time instead`,
|
||||||
|
);
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
async finalizePaymentSuccess(input: {
|
async finalizePaymentSuccess(input: {
|
||||||
intentId: string;
|
intentId: string;
|
||||||
providerTxnId?: string;
|
providerTxnId?: string;
|
||||||
@@ -477,7 +538,7 @@ export class PaymentsService {
|
|||||||
});
|
});
|
||||||
if (!booking) throw new NotFoundException("Booking not found");
|
if (!booking) throw new NotFoundException("Booking not found");
|
||||||
|
|
||||||
const paidAt = input.paidAt ?? new Date();
|
const paidAt = this.sanitizePaidAt(input.paidAt);
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
await tx.paymentIntent.update({
|
await tx.paymentIntent.update({
|
||||||
where: { id: intent.id },
|
where: { id: intent.id },
|
||||||
|
|||||||
@@ -86,6 +86,21 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
requestBody,
|
requestBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Waafi returns transaction info (params.status) ONLY when responseCode is 2001. For an
|
||||||
|
// unpaid or not-yet-existing transaction it returns an error envelope (e.g. 5001 / E10206
|
||||||
|
// "Failed to get transaction info") with no status. Treat that as still-pending (PROCESSING),
|
||||||
|
// never terminal — so the intent keeps waiting for the webhook / its expiry rather than being
|
||||||
|
// wrongly resolved off a "no info" response.
|
||||||
|
if (response.responseCode !== WAAFI_SUCCESS_CODE) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Waafi HPP_GETTRANINFO ${merchantOrderId}: ${response.responseCode}/${response.errorCode} ${response.responseMsg} — treating as pending`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: ProviderPaymentStatus.PROCESSING,
|
||||||
|
rawResponse: response as unknown as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const rawState = response.params?.status ?? response.params?.tranStatusDesc;
|
const rawState = response.params?.status ?? response.params?.tranStatusDesc;
|
||||||
const transactionId = response.params?.transactionId;
|
const transactionId = response.params?.transactionId;
|
||||||
const mapped = this.mapStatus(rawState);
|
const mapped = this.mapStatus(rawState);
|
||||||
|
|||||||
Reference in New Issue
Block a user