From 6e4b70be34e85223d3b191e40fb8ba2a5059154d Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 5 Jun 2026 11:28:43 +0300 Subject: [PATCH 1/6] chore: install date-fns --- apps/edr-freight-web/portal/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index 3cd617e0a..b9b33d35d 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^3.6.0", "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "19.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7270938b5..6a69f3c38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 lucide-react: specifier: ^1.14.0 version: 1.16.0(react@19.2.6) From f77dd543907f77a7bc1ee19094f505d39973a103 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 5 Jun 2026 11:36:13 +0300 Subject: [PATCH 2/6] feat: setup the recent booking endpoint and ui fixes --- .../portal/src/pages/MyPortalPage.tsx | 128 +++---- .../src/pages/bookings/BookingDetailPage.tsx | 311 ++++++++++-------- .../portal/src/pages/bookings/MyBookings.tsx | 54 +-- .../portal/src/services/api.ts | 3 +- .../portal/src/services/bookings.service.ts | 14 +- packages/types/src/freight/index.ts | 24 +- 6 files changed, 303 insertions(+), 231 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index 6cb111bb7..b6b17e71f 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { format } from "date-fns"; +import { useQuery } from "@tanstack/react-query"; import { ArrowRight, Building2, @@ -7,6 +9,7 @@ import { Clock, DollarSign, Eye, + LoaderCircle, Mail, MapPin, Package, @@ -20,14 +23,12 @@ import { import { getCurrentCustomer, - getMyBookings, getMyInvoices, getMyShipments, } from "@/lib/currentCustomer"; import { formatCurrency } from "@/pages/billing/invoices.mock"; import type { ShipmentStatus } from "@/pages/tracking/shipments.mock"; import type { InvoiceStatus } from "@/pages/billing/invoices.mock"; -import type { BookingStatus } from "@/pages/bookings/bookings.mock"; import { Button, Card, @@ -36,16 +37,36 @@ import { CardTitle, CardDescription, } from "@edr/ui-common"; +import { api } from "@/services/api"; + +const ACTIVE_STATUSES = [ + "DRAFT", + "SUBMITTED", + "PENDING_APPROVAL", + "IN_TRANSIT", +]; export default function MyPortalPage() { const me = useMemo(() => getCurrentCustomer(), []); - const myBookings = useMemo(() => getMyBookings(), []); const myShipments = useMemo(() => getMyShipments(), []); const myInvoices = useMemo(() => getMyInvoices(), []); - const activeBookings = myBookings.filter( - (b) => b.status === "Confirmed" || b.status === "In Transit", + const navigate = useNavigate(); + + const bookingsQuery = useQuery( + api.bookings.list.queryOptions({ + input: { sortBy: "createdAt", sortOrder: "DESC" }, + }), ); + + const myBookings = useMemo( + () => + (bookingsQuery.data?.items ?? []).filter((b) => + ACTIVE_STATUSES.includes(b.status), + ), + [bookingsQuery.data], + ); + const activeShipments = myShipments.filter((s) => s.status === "In Transit"); const outstandingInvoices = myInvoices.filter( (inv) => inv.status === "Sent" || inv.status === "Overdue", @@ -58,7 +79,7 @@ export default function MyPortalPage() { .filter((inv) => inv.status === "Paid" && inv.currency === "USD") .reduce((sum, inv) => sum + inv.amount, 0); - const recentBookings = [...myBookings].slice(0, 5); + const recentBookings = myBookings.slice(0, 5); const recentInvoices = [...myInvoices].slice(0, 4); return ( @@ -117,7 +138,7 @@ export default function MyPortalPage() { + + + + + + +`; + } + + + 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 5/6] 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 6/6] 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;