From 6786bdd210dce0c7791f1cc39b5c50b29f803ac9 Mon Sep 17 00:00:00 2001 From: Tria Date: Fri, 5 Jun 2026 17:23:21 +0300 Subject: [PATCH] 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);