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;