mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #101 from Tria-plc/freight/feature/payment
Freight/feature/payment
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -26,6 +26,9 @@ export class PaymentEntity extends BaseEntity {
|
||||
@Column({ type: "numeric" })
|
||||
amount!: number
|
||||
|
||||
@Column({ type: "varchar", length: 255, name: "reason", })
|
||||
reason?: string;
|
||||
|
||||
@Column({ type: "jsonb", default: {}, name: "raw_initiation" })
|
||||
rawInitiation?: Record<string, unknown>
|
||||
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
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"
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
|
||||
@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 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(),
|
||||
@@ -31,6 +46,7 @@ export class PaymentController {
|
||||
if (!payment) {
|
||||
throw new NotFoundException('payment not found')
|
||||
}
|
||||
|
||||
return res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -49,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)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ export class PaymentRepository {
|
||||
this.paymentRepo = this.dataSource.getRepository(PaymentEntity)
|
||||
}
|
||||
|
||||
async createTr(qr: QueryRunner, data: Pick<PaymentEntity, "amount" | "method" | "currency" | "type" | "refId" | "merchantOrderId" | "rawInitiation" | "clientAction" | "expiresAt">): Promise<PaymentEntity> {
|
||||
async createTr(qr: QueryRunner, data: Pick<PaymentEntity, "amount" | "method" | "currency" | "type" | "refId" | "merchantOrderId" | "rawInitiation" | "clientAction" | "expiresAt" | "reason">): Promise<PaymentEntity> {
|
||||
const payment = qr.manager.create(PaymentEntity, data)
|
||||
return qr.manager.save(payment)
|
||||
}
|
||||
@@ -35,5 +35,5 @@ export class PaymentRepository {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { 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,7 +6,11 @@ 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';
|
||||
|
||||
|
||||
type PaymentMethod = PaymentEntity["method"]
|
||||
type CurrencyType = PaymentEntity["currency"]
|
||||
@@ -24,7 +28,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 +37,7 @@ export class PaymentService {
|
||||
failureMessage?: string,
|
||||
}> {
|
||||
|
||||
|
||||
const strategy = this.strategies.get(method)
|
||||
if (!strategy) {
|
||||
throw new NotFoundException("strategy not found")
|
||||
@@ -40,6 +45,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 +68,8 @@ export class PaymentService {
|
||||
merchantOrderId: orderId,
|
||||
rawInitiation: paymentResp.rawInitiation,
|
||||
clientAction: paymentResp.clientAction,
|
||||
expiresAt: paymentResp.expiresAt
|
||||
expiresAt: paymentResp.expiresAt,
|
||||
reason
|
||||
|
||||
})
|
||||
await queryRunner.commitTransaction()
|
||||
@@ -88,26 +95,47 @@ export class PaymentService {
|
||||
return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method)
|
||||
}
|
||||
|
||||
|
||||
async handleTelebirrPaymentCb(dto: UpdatePaymentStatusDto): Promise<void> {
|
||||
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;
|
||||
|
||||
async genReceiptHtml(orderId: string) {
|
||||
const payment = await this.paymentRepo.findOneBy({
|
||||
merchantOrderId: orderId,
|
||||
status: "success"
|
||||
})
|
||||
if (!payment) {
|
||||
throw new BadRequestException()
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const html = template({
|
||||
vendorName: "Ethio Djibouti Railway Ticket Booking",
|
||||
vendorAddress: "Addis Ababa",
|
||||
receiptDate: payment.paidAt,
|
||||
paymentMethod: payment?.method,
|
||||
subtotal: payment?.amount.toString(),
|
||||
total: payment?.amount.toString(),
|
||||
currency: payment?.currency,
|
||||
reason: payment?.reason
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
}
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ export class PaymentTelebirrStrategy implements PaymentStrategy {
|
||||
async pay(data: ProviderInitiationInput): Promise<any> {
|
||||
// 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<ProviderInitiationResult> {
|
||||
async initiate(redirectURL: string, input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting...</p>
|
||||
|
||||
<script>
|
||||
window.location.href = "{{url}}";
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
191
apps/edr-freight-api/src/modules/payment/templates/receipt.hbs
Normal file
191
apps/edr-freight-api/src/modules/payment/templates/receipt.hbs
Normal file
@@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Receipt - {{vendorName}}</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.receipt-box {
|
||||
max-width: 550px;
|
||||
margin: auto;
|
||||
padding: 30px;
|
||||
border: 1px solid #eee;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px dashed #eee;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 22px;
|
||||
margin: 0 0 5px;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.details-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #777;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.totals-section {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.download-container {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: #0056b3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #004494;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.receipt-box {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.download-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="receipt-box">
|
||||
<div class="header">
|
||||
<h1>{{vendorName}}</h1>
|
||||
<p>{{vendorAddress}}</p>
|
||||
</div>
|
||||
|
||||
<table class="details-table">
|
||||
<tr>
|
||||
<td class="label">Date</td>
|
||||
<td class="value">{{receiptDate}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="label">Payment Method</td>
|
||||
<td class="value" style="text-transform: capitalize;">
|
||||
{{paymentMethod}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="label">Description</td>
|
||||
<td class="value">{{reason}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="totals-section">
|
||||
<table class="details-table">
|
||||
<tr>
|
||||
<td class="label">Subtotal</td>
|
||||
<td class="value">
|
||||
{{currency}} {{subtotal}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="total-row">
|
||||
<td class="label" style="color:#333">
|
||||
Total Paid
|
||||
</td>
|
||||
<td class="value" style="color:#0056b3">
|
||||
{{currency}} {{total}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Thank you for traveling with us!</p>
|
||||
<p>Have a safe journey.</p>
|
||||
</div>
|
||||
|
||||
<div class="download-container">
|
||||
<button class="download-btn" onclick="downloadPdf()">
|
||||
Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function downloadPdf() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,33 @@ export class TelebirrWebhookService {
|
||||
}
|
||||
|
||||
async handle(payload: TelebirrDto): Promise<void> {
|
||||
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":
|
||||
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", })
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user