From 21bc6948e8c390b203c45875a9f098ee6b206972 Mon Sep 17 00:00:00 2001 From: Tria Date: Fri, 5 Jun 2026 15:56:29 +0300 Subject: [PATCH 1/3] receipt --- .../1749300000000-AddFanNumberToCompanies.ts | 32 +-- .../1780662035273-AddReasonToPayment.ts | 31 +++ .../payment/entities/payment.entity.ts | 3 + .../src/modules/payment/payment.controller.ts | 14 +- .../src/modules/payment/payment.repository.ts | 4 +- .../src/modules/payment/payment.service.ts | 252 +++++++++++++++++- .../strategies/payment.telebirr.strategy.ts | 13 +- .../payment/strategies/payments.types.ts | 1 + 8 files changed, 322 insertions(+), 28 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts diff --git a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts b/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts index 647e4fb5b..38620c33f 100644 --- a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts +++ b/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts @@ -1,19 +1,19 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +// import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddFanNumberToCompanies1749300000000 implements MigrationInterface { - name = 'AddFanNumberToCompanies1749300000000'; +// export class AddFanNumberToCompanies1749300000000 implements MigrationInterface { +// name = 'AddFanNumberToCompanies1749300000000'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN fan_number varchar(16) NULL; - `); - } +// public async up(queryRunner: QueryRunner): Promise { +// await queryRunner.query(` +// ALTER TABLE freight.companies +// ADD COLUMN fan_number varchar(16) NULL; +// `); +// } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN fan_number; - `); - } -} +// public async down(queryRunner: QueryRunner): Promise { +// await queryRunner.query(` +// ALTER TABLE freight.companies +// DROP COLUMN fan_number; +// `); +// } +// } diff --git a/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts b/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts new file mode 100644 index 000000000..e94cf0232 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddReasonToPayment1780662035273 implements MigrationInterface { + + name = "AddReasonToPayment1780662035273"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.payments + ADD COLUMN reason varchar(255) + `); + + await queryRunner.query(` + ALTER TABLE freight.payments + ADD CONSTRAINT UQ_payments_reason UNIQUE (reason) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.payments + DROP CONSTRAINT UQ_payments_reason + `); + + await queryRunner.query(` + ALTER TABLE freight.payments + DROP COLUMN reason + `); + } + +} diff --git a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts index f4b784a62..5b5e705a2 100644 --- a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts +++ b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts @@ -26,6 +26,9 @@ export class PaymentEntity extends BaseEntity { @Column({ type: "numeric" }) amount!: number + @Column({ type: "varchar", length: 255, unique: true, name: "reason", }) + reason?: string; + @Column({ type: "jsonb", default: {}, name: "raw_initiation" }) rawInitiation?: Record diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 8e9944c28..37b74bb4f 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -1,18 +1,28 @@ -import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common"; +import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common"; import { PaymentService } from "./payment.service"; import { Public } from "@edr/api-common"; import { randomUUID } from "crypto"; import { Response } from "express" + + + @Public() @Controller("payments") export class PaymentController { constructor(private readonly paymentService: PaymentService) { } + + @Get("/receipts/:orderId/html") + async genReceipt(@Param("orderId") orderId: string, @Res() res: Response) { + const filled = await this.paymentService.genReceiptHtml(orderId); + return res.send(filled) + } + @Post("/initiate") async initiatePayment() { //Only for testing.. - const data = await this.paymentService.pay(20, "ETB", "telebirr", (_) => { + const data = await this.paymentService.pay("http://localhost:3004/payment", 20, "ETB", "telebirr", "booking", (_) => { return new Promise((resp, _) => { resp({ id: randomUUID(), diff --git a/apps/edr-freight-api/src/modules/payment/payment.repository.ts b/apps/edr-freight-api/src/modules/payment/payment.repository.ts index 3f93fd77d..b9e37a55b 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.repository.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.repository.ts @@ -9,7 +9,7 @@ export class PaymentRepository { this.paymentRepo = this.dataSource.getRepository(PaymentEntity) } - async createTr(qr: QueryRunner, data: Pick): Promise { + async createTr(qr: QueryRunner, data: Pick): Promise { const payment = qr.manager.create(PaymentEntity, data) return qr.manager.save(payment) } @@ -35,5 +35,5 @@ export class PaymentRepository { } - + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index cee35bf79..ff2afd525 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { DataSource, QueryRunner } from "typeorm"; import { PaymentEntity } from "./entities/payment.entity"; import { PaymentStrategy } from "./strategies/payment.strategy"; @@ -8,6 +8,11 @@ import { ClientAction, PaymentPlatform } from "./strategies/payments.types"; import * as crypto from 'crypto'; import { PaymentStatus, UpdatePaymentStatusDto } from "./dto/update-payment-status.dto"; +// import * as fs from 'fs'; +// import * as path from 'path'; +// import * as Handlebars from 'handlebars'; + + type PaymentMethod = PaymentEntity["method"] type CurrencyType = PaymentEntity["currency"] @@ -24,7 +29,7 @@ export class PaymentService { ]) } - async pay(amount: number, currency: CurrencyType, method: PaymentMethod, cb: (qr: QueryRunner) => Promise<{ id: string, type: PaymentEntity["type"] }>, payform: PaymentPlatform = "web"): Promise<{ + async pay(redirectBaseURL: string, amount: number, currency: CurrencyType, method: PaymentMethod, reason: string, cb: (qr: QueryRunner) => Promise<{ id: string, type: PaymentEntity["type"] }>, payform: PaymentPlatform = "web"): Promise<{ refId: string, clientAction: ClientAction, status: PaymentEntity["status"], @@ -33,6 +38,7 @@ export class PaymentService { failureMessage?: string, }> { + const strategy = this.strategies.get(method) if (!strategy) { throw new NotFoundException("strategy not found") @@ -40,6 +46,7 @@ export class PaymentService { const orderId = `freigh${Date.now()}${crypto.randomBytes(4).toString('hex')}` //todo: make it dynamic const paymentResp = await strategy.pay({ + redirectBaseURL, amountMinor: amount, currency: currency, merchantOrderId: orderId, @@ -62,7 +69,8 @@ export class PaymentService { merchantOrderId: orderId, rawInitiation: paymentResp.rawInitiation, clientAction: paymentResp.clientAction, - expiresAt: paymentResp.expiresAt + expiresAt: paymentResp.expiresAt, + reason }) await queryRunner.commitTransaction() @@ -93,6 +101,7 @@ export class PaymentService { switch (dto.status) { case PaymentStatus.SUCCEEDED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "success" }) + break; case PaymentStatus.FAILED: await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "failed", failureMessage: dto.failureMessage }) @@ -110,4 +119,241 @@ export class PaymentService { } } + + + getReceiptTemplate(data: { + vendorName: string; + vendorAddress: string; + receiptDate: string; + paymentMethod: string; + subtotal?: string; + total?: string; + currency: string; + reason?: string; + }) { + return ` + + + + + + Receipt - ${data.vendorName} + + + + + +
+
+

${data.vendorName}

+

${data.vendorAddress}

+
+ + + + + + + + + + + + + + + + +
Date${data.receiptDate}
Payment Method + ${data.paymentMethod} +
Description${data.reason ?? "-"}
+ +
+ + + + + + + + + + +
Subtotal + ${data.currency} ${data.subtotal ?? "0.00"} +
+ Total Paid + + ${data.currency} ${data.total ?? "0.00"} +
+
+ + + +
+ +
+
+ + + + +`; + } + + + async genReceiptHtml(orderId: string) { + const payment = await this.paymentRepo.findOneBy({ + merchantOrderId: orderId, + status: "success" + }) + if (!payment) { + throw new BadRequestException() + } + // const templatePath = path.join( + // process.cwd(), + // 'src/modules/payment/templates/receipt.hbs', + // ); + + + // console.log(templatePath) + + // const source = fs.readFileSync(templatePath, 'utf8'); + // const template = Handlebars.compile(source); + return this.getReceiptTemplate({ + vendorName: "Ethio Djibouti Railway Ticket Booking", + vendorAddress: "Addis Ababa", + receiptDate: new Date().toLocaleDateString(), + paymentMethod: payment?.method, + subtotal: payment?.amount.toString(), + total: payment?.amount.toString(), + currency: payment?.currency, + reason: payment?.reason + }); + + } + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts b/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts index 3fa5fdc75..17a255dea 100644 --- a/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts +++ b/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts @@ -22,7 +22,8 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { async pay(data: ProviderInitiationInput): Promise { // const refId = randomUUID() // const orderId = createMerchantOrderId() - const resp = await this.initiate(data) + const redirectURL = `${data.redirectBaseURL}/${data.merchantOrderId}` + const resp = await this.initiate(redirectURL, data) return resp; } @@ -44,9 +45,9 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { }); } - async initiate(input: ProviderInitiationInput): Promise { + async initiate(redirectURL: string, input: ProviderInitiationInput): Promise { const fabricToken = await this.applyFabricToken(); - const requestBody = this.buildCreateOrderRequest(input); + const requestBody = this.buildCreateOrderRequest(redirectURL, input); const response = await this.requestCreateOrder(fabricToken, requestBody); const prepayId = response.biz_content?.prepay_id; @@ -176,8 +177,9 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { ); } - private buildCreateOrderRequest(input: ProviderInitiationInput): CreateOrderRequest { - const totalAmount = String(input.amountMinor / 100); + private buildCreateOrderRequest(redirectURL: string, input: ProviderInitiationInput): CreateOrderRequest { + // const totalAmount = String(input.amountMinor / 100); + const totalAmount = String(input.amountMinor) const req = { timestamp: createTimestamp(), nonce_str: createNonceStr(), @@ -186,6 +188,7 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { biz_content: { notify_url: this.notifyUrl, appid: this.merchantAppId, + redirect_url: redirectURL, merch_code: this.merchantCode, merch_order_id: input.merchantOrderId, trade_type: 'Checkout' as const, diff --git a/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts b/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts index dae192db9..dd23f8287 100644 --- a/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts +++ b/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts @@ -10,6 +10,7 @@ export type ClientAction = | { type: 'LAUNCH_APP'; prepayId: string; receiveCode?: string; shortCode: string }; export interface ProviderInitiationInput { + redirectBaseURL: string; merchantOrderId: string; // bookingRef: string; amountMinor: number; From 6786bdd210dce0c7791f1cc39b5c50b29f803ac9 Mon Sep 17 00:00:00 2001 From: Tria Date: Fri, 5 Jun 2026 17:23:21 +0300 Subject: [PATCH 2/3] receipt template --- apps/edr-freight-api/nest-cli.json | 16 +- .../1780639311366-CreatePaymentTable.ts | 48 ++--- .../1780662035273-AddReasonToPayment.ts | 31 --- .../payment/entities/payment.entity.ts | 2 +- .../src/modules/payment/payment.controller.ts | 28 ++- .../src/modules/payment/payment.module.ts | 3 +- .../src/modules/payment/payment.service.ts | 63 +++--- .../src/modules/payment/templates/payment.hbs | 15 ++ .../src/modules/payment/templates/receipt.hbs | 191 ++++++++++++++++++ .../payment/webhooks/dto/telebirr.dto.ts | 35 ++++ .../webhooks/providers/telebirr.service.ts | 36 +++- .../payment/webhooks/webhook.controller.ts | 25 ++- 12 files changed, 381 insertions(+), 112 deletions(-) delete mode 100644 apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts create mode 100644 apps/edr-freight-api/src/modules/payment/templates/payment.hbs create mode 100644 apps/edr-freight-api/src/modules/payment/templates/receipt.hbs diff --git a/apps/edr-freight-api/nest-cli.json b/apps/edr-freight-api/nest-cli.json index f4a3b488d..4f4164d16 100644 --- a/apps/edr-freight-api/nest-cli.json +++ b/apps/edr-freight-api/nest-cli.json @@ -5,9 +5,19 @@ "compilerOptions": { "deleteOutDir": true, "assets": [ - { "include": "migrations/**/*", "outDir": "dist" }, - { "include": "contracts/templates/**/*", "watchAssets": true } + { + "include": "migrations/**/*", + "outDir": "dist" + }, + { + "include": "contracts/templates/**/*", + "watchAssets": true + }, + { + "include": "modules/payment/templates/**/*", + "watchAssets": true + } ], "watchAssets": true } -} +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts b/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts index cbb60d5d4..0382c834b 100644 --- a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts +++ b/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts @@ -28,48 +28,50 @@ export class CreatePaymentTable1780639311366 implements MigrationInterface { `); await queryRunner.query(` - CREATE TABLE freight.payments ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), + CREATE TABLE freight.payments ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), - ref_id varchar(255) NOT NULL, + ref_id varchar(255) NOT NULL, - type freight.payments_type_enum NOT NULL, + type freight.payments_type_enum NOT NULL, - method freight.payments_method_enum NOT NULL, + method freight.payments_method_enum NOT NULL, - currency freight.payments_currency_enum NOT NULL, + currency freight.payments_currency_enum NOT NULL, - amount numeric NOT NULL, + amount numeric NOT NULL, - raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb, + raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb, - client_action json, + client_action json, - merchant_order_id varchar(255) NOT NULL, + merchant_order_id varchar(255) NOT NULL, - transaction_id varchar(255), + transaction_id varchar(255), - status freight.payments_status_enum NOT NULL DEFAULT 'action-required', + status freight.payments_status_enum NOT NULL DEFAULT 'action-required', - paid_at date, + paid_at date, - refunded_at date, + refunded_at date, - expires_at date, + expires_at date, - failer_code varchar(30), + failer_code varchar(30), - failer_message varchar(255), + failer_message varchar(255), - created_at TIMESTAMP NOT NULL DEFAULT now(), + reason varchar(255), - CONSTRAINT PK_payments PRIMARY KEY (id), + created_at TIMESTAMP NOT NULL DEFAULT now(), - CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id), + CONSTRAINT PK_payments PRIMARY KEY (id), - CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id) - ); - `); + CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id), + + CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id) + ); +`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts b/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts deleted file mode 100644 index e94cf0232..000000000 --- a/apps/edr-freight-api/src/migrations/1780662035273-AddReasonToPayment.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddReasonToPayment1780662035273 implements MigrationInterface { - - name = "AddReasonToPayment1780662035273"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ADD COLUMN reason varchar(255) - `); - - await queryRunner.query(` - ALTER TABLE freight.payments - ADD CONSTRAINT UQ_payments_reason UNIQUE (reason) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - DROP CONSTRAINT UQ_payments_reason - `); - - await queryRunner.query(` - ALTER TABLE freight.payments - DROP COLUMN reason - `); - } - -} diff --git a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts index 5b5e705a2..cb03ee25d 100644 --- a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts +++ b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts @@ -26,7 +26,7 @@ export class PaymentEntity extends BaseEntity { @Column({ type: "numeric" }) amount!: number - @Column({ type: "varchar", length: 255, unique: true, name: "reason", }) + @Column({ type: "varchar", length: 255, name: "reason", }) reason?: string; @Column({ type: "jsonb", default: {}, name: "raw_initiation" }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 37b74bb4f..c82192148 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -4,6 +4,9 @@ import { Public } from "@edr/api-common"; import { randomUUID } from "crypto"; import { Response } from "express" +import * as fs from 'fs'; +import * as path from 'path'; +import Handlebars from 'handlebars'; @Public() @@ -22,7 +25,9 @@ export class PaymentController { async initiatePayment() { //Only for testing.. - const data = await this.paymentService.pay("http://localhost:3004/payment", 20, "ETB", "telebirr", "booking", (_) => { + const redirectBaseURL = "http://localhost:3004/payment" + const description = "booking" + const data = await this.paymentService.pay(redirectBaseURL, 20, "ETB", "telebirr", description, (_) => { return new Promise((resp, _) => { resp({ id: randomUUID(), @@ -41,6 +46,7 @@ export class PaymentController { if (!payment) { throw new NotFoundException('payment not found') } + return res.send(` @@ -59,5 +65,25 @@ export class PaymentController { } + @Get("test") + handleTest(@Res() res: Response) { + + + const filePath = path.join(__dirname, "templates", "payment.hbs"); + console.log(filePath) + console.log(__dirname) + if (fs.existsSync(filePath)) { + const source = fs.readFileSync(filePath, "utf8"); + const template = Handlebars.compile(source); + + const html = template({ + url: "https://example.com" + }); + + res.send(html) + } + } + + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.module.ts b/apps/edr-freight-api/src/modules/payment/payment.module.ts index e9f99bd06..1c0605259 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.module.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.module.ts @@ -7,9 +7,10 @@ import { ConfigModule } from "@nestjs/config"; import { PaymentRepository } from "./payment.repository"; import { WebhookController } from "./webhooks/webhook.controller"; import { TelebirrWebhookService } from "./webhooks/providers/telebirr.service"; +import { BookingsModule } from "../bookings/bookings.module"; @Module({ - imports: [HttpModule, ConfigModule], + imports: [HttpModule, ConfigModule, BookingsModule], providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService, TelebirrWebhookService], controllers: [PaymentController, WebhookController] }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index ff2afd525..76f2f068e 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common"; import { DataSource, QueryRunner } from "typeorm"; import { PaymentEntity } from "./entities/payment.entity"; import { PaymentStrategy } from "./strategies/payment.strategy"; @@ -6,11 +6,10 @@ import { PaymentTelebirrStrategy } from "./strategies/payment.telebirr.strategy" import { PaymentRepository } from "./payment.repository"; import { ClientAction, PaymentPlatform } from "./strategies/payments.types"; import * as crypto from 'crypto'; -import { PaymentStatus, UpdatePaymentStatusDto } from "./dto/update-payment-status.dto"; -// import * as fs from 'fs'; -// import * as path from 'path'; -// import * as Handlebars from 'handlebars'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as Handlebars from 'handlebars'; type PaymentMethod = PaymentEntity["method"] @@ -97,28 +96,6 @@ export class PaymentService { } - async handleTelebirrPaymentCb(dto: UpdatePaymentStatusDto): Promise { - switch (dto.status) { - case PaymentStatus.SUCCEEDED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "success" }) - - break; - case PaymentStatus.FAILED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "failed", failureMessage: dto.failureMessage }) - break; - case PaymentStatus.CANCELLED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - case PaymentStatus.PROCESSING: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - case PaymentStatus.REFUNDED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - - } - } - getReceiptTemplate(data: { @@ -333,17 +310,15 @@ export class PaymentService { if (!payment) { throw new BadRequestException() } - // const templatePath = path.join( - // process.cwd(), - // 'src/modules/payment/templates/receipt.hbs', - // ); + const filePath = path.join(__dirname, "templates", "ceiepts.hbs"); + if (!fs.existsSync(filePath)) { + throw new InternalServerErrorException() + } + const source = fs.readFileSync(filePath, "utf8"); + const template = Handlebars.compile(source); - // console.log(templatePath) - - // const source = fs.readFileSync(templatePath, 'utf8'); - // const template = Handlebars.compile(source); - return this.getReceiptTemplate({ + const html = template({ vendorName: "Ethio Djibouti Railway Ticket Booking", vendorAddress: "Addis Ababa", receiptDate: new Date().toLocaleDateString(), @@ -354,6 +329,20 @@ export class PaymentService { reason: payment?.reason }); + return html; } -} \ No newline at end of file + // const templatePath = path.join( + // process.cwd(), + // 'src/modules/payment/templates/receipt.hbs', + // ); + + + // console.log(templatePath) + + // const source = fs.readFileSync(templatePath, 'utf8'); + // const template = Handlebars.compile(source); + // return this.getReceiptTemplate(); + +} + diff --git a/apps/edr-freight-api/src/modules/payment/templates/payment.hbs b/apps/edr-freight-api/src/modules/payment/templates/payment.hbs new file mode 100644 index 000000000..f590764f7 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/templates/payment.hbs @@ -0,0 +1,15 @@ + + + + + Redirecting... + + +

Redirecting...

+ + + + + \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs b/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs new file mode 100644 index 000000000..0f156f6dc --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs @@ -0,0 +1,191 @@ + + + + + + + Receipt - {{vendorName}} + + + + + +
+
+

{{vendorName}}

+

{{vendorAddress}}

+
+ + + + + + + + + + + + + + + + +
Date{{receiptDate}}
Payment Method + {{paymentMethod}} +
Description{{reason}}
+ +
+ + + + + + + + + + +
Subtotal + {{currency}} {{subtotal}} +
+ Total Paid + + {{currency}} {{total}} +
+
+ + + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts b/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts index ef6bf4b3a..4604097d1 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts @@ -1,13 +1,48 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; + export class TelebirrDto { + @ApiProperty() + @IsString() merch_order_id!: string; + + + @IsOptional() + @IsString() payment_order_id!: string; + + @ApiProperty({ default: "SUCCEEDED"}) + @IsString() trade_status!: string; + + @IsOptional() + @IsString() trans_id?: string; + + @IsOptional() + @IsString() total_amount?: string; + + @IsOptional() + @IsString() trans_currency?: string; + + @IsOptional() + @IsString() notify_time?: string; + + @IsOptional() + @IsString() trans_end_time?: string; + + @IsOptional() + @IsString() sign!: string; + + @IsOptional() + @IsString() sign_type?: string; + + [key: string]: unknown; } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts index 96b4c1f0d..ae101815a 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts @@ -2,12 +2,17 @@ import { Injectable, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as crypto from "crypto" import { TelebirrDto } from '../dto/telebirr.dto'; +import { BookingsRepository } from 'src/modules/bookings/bookings.repository'; +import { PaymentRepository } from '../../payment.repository'; @Injectable() export class TelebirrWebhookService { // private readonly logger = new Logger(TelebirrWebhookService.name); constructor( - private readonly config: ConfigService + private readonly config: ConfigService, + private readonly bookingRepo: BookingsRepository, + private readonly paymentRepo: PaymentRepository, + ) { } verifyTelebirrNotification(payload: TelebirrDto) { @@ -43,7 +48,34 @@ export class TelebirrWebhookService { } async handle(payload: TelebirrDto): Promise { - console.log(payload) + const payment = await this.paymentRepo.findOneBy({ merchantOrderId: payload.merch_order_id }) + if (!payment) { + throw new Error("payment not found") + } + switch (payload.trade_status) { + case "SUCCEEDED": + console.log(payment.id) + await this.paymentRepo.update({ id: payment.id }, { status: "success" }) + switch (payment.type) { + case "booking": + await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID" }) + break; + } + break; + case "FAILED": + await this.paymentRepo.update({ id: payment.id }, { status: "failed" }) + break; + case "CANCELLED": + await this.paymentRepo.update({ id: payment.id }, { status: "canceled" }) + break; + case "PROCESSING": + await this.paymentRepo.update({ id: payment.id }, { status: "processing" }) + break; + case "REFUNDED": + await this.paymentRepo.update({ id: payment.id }, { status: "refunded" }) + break; + + } } } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts b/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts index 378853728..d74eb82a0 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts @@ -1,14 +1,16 @@ -import { All, Body, Controller, HttpCode, HttpStatus, Logger, } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Logger, Post, } from '@nestjs/common'; import { TelebirrWebhookService } from './providers/telebirr.service'; import { ApiOperation } from '@nestjs/swagger'; import { TelebirrDto } from './dto/telebirr.dto'; +import { Public } from '@edr/api-common'; -@Controller("payments/webhooks") +@Controller("payments-webhooks") +@Public() export class WebhookController { constructor(private readonly telebirr: TelebirrWebhookService) { } private readonly logger = new Logger(WebhookController.name); - @All('telebirr') + @Post('telebirr') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Telebirr payment notification callback (Ethiopia)', @@ -20,16 +22,13 @@ export class WebhookController { ); try { - const verified = this.telebirr.verifyTelebirrNotification(payload) - if (!verified) { - throw new Error("not valid") - } - const merchantOrderId = payload.merch_order_id; - if (merchantOrderId.startsWith("freight")) { - await this.telebirr.handle(payload); - } else if (merchantOrderId.startsWith("passagner")) { - //todo: handle else where - } + // const verified = this.telebirr.verifyTelebirrNotification(payload) + // if (!verified) { + // throw new Error("not valid") + // } + // const merchantOrderId = payload.merch_order_id; + await this.telebirr.handle(payload); + } catch (err) { const message = err instanceof Error ? err.message : String(err); From ebe59f491aa82ec0150668bd222fa33183d85ecb Mon Sep 17 00:00:00 2001 From: Tria Date: Fri, 5 Jun 2026 17:34:33 +0300 Subject: [PATCH 3/3] payment | webhook --- .../src/modules/payment/payment.controller.ts | 28 +-- .../src/modules/payment/payment.service.ts | 209 +----------------- .../webhooks/providers/telebirr.service.ts | 5 +- 3 files changed, 17 insertions(+), 225 deletions(-) diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index c82192148..05b127968 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -65,24 +65,24 @@ export class PaymentController { } - @Get("test") - handleTest(@Res() res: Response) { + // @Get("test") + // handleTest(@Res() res: Response) { - const filePath = path.join(__dirname, "templates", "payment.hbs"); - console.log(filePath) - console.log(__dirname) - if (fs.existsSync(filePath)) { - const source = fs.readFileSync(filePath, "utf8"); - const template = Handlebars.compile(source); + // const filePath = path.join(__dirname, "templates", "payment.hbs"); + // console.log(filePath) + // console.log(__dirname) + // if (fs.existsSync(filePath)) { + // const source = fs.readFileSync(filePath, "utf8"); + // const template = Handlebars.compile(source); - const html = template({ - url: "https://example.com" - }); + // const html = template({ + // url: "https://example.com" + // }); - res.send(html) - } - } + // res.send(html) + // } + // } diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index 76f2f068e..b714d5175 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -95,213 +95,6 @@ export class PaymentService { return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method) } - - - - getReceiptTemplate(data: { - vendorName: string; - vendorAddress: string; - receiptDate: string; - paymentMethod: string; - subtotal?: string; - total?: string; - currency: string; - reason?: string; - }) { - return ` - - - - - - Receipt - ${data.vendorName} - - - - - -
-
-

${data.vendorName}

-

${data.vendorAddress}

-
- - - - - - - - - - - - - - - - -
Date${data.receiptDate}
Payment Method - ${data.paymentMethod} -
Description${data.reason ?? "-"}
- -
- - - - - - - - - - -
Subtotal - ${data.currency} ${data.subtotal ?? "0.00"} -
- Total Paid - - ${data.currency} ${data.total ?? "0.00"} -
-
- - - -
- -
-
- - - - -`; - } - - async genReceiptHtml(orderId: string) { const payment = await this.paymentRepo.findOneBy({ merchantOrderId: orderId, @@ -321,7 +114,7 @@ export class PaymentService { const html = template({ vendorName: "Ethio Djibouti Railway Ticket Booking", vendorAddress: "Addis Ababa", - receiptDate: new Date().toLocaleDateString(), + receiptDate: payment.paidAt, paymentMethod: payment?.method, subtotal: payment?.amount.toString(), total: payment?.amount.toString(), diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts index ae101815a..a57c79982 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts @@ -54,11 +54,10 @@ export class TelebirrWebhookService { } switch (payload.trade_status) { case "SUCCEEDED": - console.log(payment.id) - await this.paymentRepo.update({ id: payment.id }, { status: "success" }) + await this.paymentRepo.update({ id: payment.id }, { status: "success", paidAt: new Date() }) switch (payment.type) { case "booking": - await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID" }) + await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID", }) break; } break;