import { All, Body, Controller, Headers, HttpCode, HttpStatus, Logger, Post, Req, } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { CardWebhookPayload, CbeBirrWebhookPayload, DMoneyWebhookPayload, EBirrWebhookPayload, TelebirrWebhookPayload, WaafiWebhookHeaders, WaafiWebhookPayload, } from "@edr/payment-providers"; import { TelebirrWebhookService } from "./handlers/telebirr-webhook.service"; import { CbeBirrWebhookService } from "./handlers/cbe-birr-webhook.service"; import { EBirrWebhookService } from "./handlers/ebirr-webhook.service"; import { CardWebhookService } from "./handlers/card-webhook.service"; import { WaafiWebhookService } from "./handlers/waafi-webhook.service"; import { DMoneyWebhookService } from "./handlers/dmoney-webhook.service"; /** * The ONLY public surface of the payment service — the single registered webhook URL per * provider for the whole platform. No service auth here (provider-facing); trust comes from * signature verification inside each handler. Every route acks 2xx fast and never rethrows: * Waafi times out at 5s and does NOT retry. */ @ApiTags("Provider Webhooks") @Controller("webhooks") export class WebhooksController { private readonly logger = new Logger(WebhooksController.name); constructor( private readonly telebirr: TelebirrWebhookService, private readonly cbeBirr: CbeBirrWebhookService, private readonly eBirr: EBirrWebhookService, private readonly card: CardWebhookService, private readonly waafi: WaafiWebhookService, private readonly dMoney: DMoneyWebhookService, ) {} @All("telebirr") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Telebirr payment notification callback (Ethiopia)", }) async receiveTelebirr(@Body() payload: TelebirrWebhookPayload) { this.logger.log("Telebirr webhook called"); try { await this.telebirr.handle(payload); } catch (err) { this.logger.error(`Telebirr webhook handler threw: ${this.message(err)}`); } return { code: "0", message: "OK" }; } @Post("cbe-birr") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "CBE Birr payment notification callback (Ethiopia)", }) async receiveCbeBirr(@Body() payload: CbeBirrWebhookPayload) { try { await this.cbeBirr.handle(payload); } catch (err) { this.logger.error(`CBE Birr webhook handler threw: ${this.message(err)}`); } return { success: true }; } @Post("ebirr") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "eBirr payment notification callback (Ethiopia)" }) async receiveEBirr(@Body() payload: EBirrWebhookPayload) { try { await this.eBirr.handle(payload); } catch (err) { this.logger.error(`eBirr webhook handler threw: ${this.message(err)}`); } return { code: "0000", message: "success" }; } @Post("card") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Card payment notification callback (International)", }) async receiveCard( @Body() payload: CardWebhookPayload, @Headers("stripe-signature") signature: string, ) { try { await this.card.handle(payload, signature); } catch (err) { this.logger.error(`Card webhook handler threw: ${this.message(err)}`); } return { received: true }; } @Post("waafi") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Waafi payment notification callback (Djibouti)" }) async receiveWaafi( @Body() payload: WaafiWebhookPayload, @Headers() headers: WaafiWebhookHeaders, @Req() req: { rawBody?: Buffer }, ) { this.logger.log("\n\n\n\nWaafi payment notification callback (Djibouti)\n\n\n\n"); this.logger.log( `Waafi webhook hit: event=${payload?.event ?? "unknown"} eventId=${headers["x-webhook-event-id"] ?? "n/a"}`, ); try { // HMAC verification must sign over the exact raw bytes Waafi sent, not re-serialized JSON. const rawBody = req.rawBody?.toString("utf8") ?? ""; await this.waafi.handle(payload, rawBody, headers); } catch (err) { this.logger.error(`Waafi webhook handler threw: ${this.message(err)}`); } return { responseCode: "2001", responseMsg: "Success" }; } @Post("dmoney") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "D-Money payment notification callback (Djibouti)" }) async receiveDMoney(@Body() payload: DMoneyWebhookPayload) { try { await this.dMoney.handle(payload); } catch (err) { this.logger.error(`D-Money webhook handler threw: ${this.message(err)}`); } return { code: "0", msg: "Success", result: "SUCCESS" }; } private message(err: unknown): string { return err instanceof Error ? err.message : String(err); } }