mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
payment
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export enum PaymentStatus {
|
||||
REQUIRES_ACTION,
|
||||
PROCESSING,
|
||||
SUCCEEDED,
|
||||
FAILED,
|
||||
CANCELLED,
|
||||
REFUNDED,
|
||||
|
||||
}
|
||||
export class UpdatePaymentStatusDto {
|
||||
@IsString()
|
||||
orderId!: string;
|
||||
|
||||
|
||||
@IsEnum(PaymentStatus)
|
||||
status!: PaymentStatus
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
failureMessage?: string
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common";
|
||||
import { Body, 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 { UpdatePaymentStatusDto } from "./dto/update-payment-status.dto";
|
||||
@Public()
|
||||
@Controller("payments")
|
||||
export class PaymentController {
|
||||
@@ -49,9 +50,5 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
|
||||
@Post("web-hooks/telebirr")
|
||||
paymentWebhook() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import { HttpModule } from "@nestjs/axios";
|
||||
import { PaymentController } from "./payment.controller";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { PaymentRepository } from "./payment.repository";
|
||||
import { WebhookController } from "./webhooks/webhook.controller";
|
||||
import { TelebirrWebhookService } from "./webhooks/providers/telebirr.service";
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule, ConfigModule],
|
||||
providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService],
|
||||
controllers: [PaymentController]
|
||||
providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService, TelebirrWebhookService],
|
||||
controllers: [PaymentController, WebhookController]
|
||||
})
|
||||
export class PaymentModule { }
|
||||
@@ -35,4 +35,5 @@ export class PaymentRepository {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { DataSource, QueryRunner } from "typeorm";
|
||||
import { DataSource, QueryRunner } from "typeorm";
|
||||
import { PaymentEntity } from "./entities/payment.entity";
|
||||
import { PaymentStrategy } from "./strategies/payment.strategy";
|
||||
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";
|
||||
|
||||
type PaymentMethod = PaymentEntity["method"]
|
||||
type CurrencyType = PaymentEntity["currency"]
|
||||
@@ -88,4 +89,25 @@ export class PaymentService {
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class TelebirrDto {
|
||||
merch_order_id!: string;
|
||||
payment_order_id!: string;
|
||||
trade_status!: string;
|
||||
trans_id?: string;
|
||||
total_amount?: string;
|
||||
trans_currency?: string;
|
||||
notify_time?: string;
|
||||
trans_end_time?: string;
|
||||
sign!: string;
|
||||
sign_type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as crypto from "crypto"
|
||||
import { TelebirrDto } from '../dto/telebirr.dto';
|
||||
@Injectable()
|
||||
export class TelebirrWebhookService {
|
||||
// private readonly logger = new Logger(TelebirrWebhookService.name);
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService
|
||||
) { }
|
||||
|
||||
verifyTelebirrNotification(payload: TelebirrDto) {
|
||||
// 1. Extract the signature provided by Telebirr
|
||||
const { sign, ...bizContent } = payload;
|
||||
|
||||
if (!sign) {
|
||||
throw new Error("Missing 'sign' field from Telebirr payload");
|
||||
}
|
||||
|
||||
// 2. Sort the remaining keys alphabetically to rebuild the raw string
|
||||
const sortedKeys = Object.keys(bizContent).sort();
|
||||
const signString = sortedKeys
|
||||
.map(key => `${key}=${typeof bizContent[key] === 'object' ? JSON.stringify(bizContent[key]) : bizContent[key]}`)
|
||||
.join('&');
|
||||
|
||||
// 3. Convert Telebirr's public key into an object specifying RSA-PSS padding
|
||||
const publicKey = {
|
||||
key: this.config.get<string>("telebirr.publicKey") ?? "",
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: 32 // Telebirr standard salt length
|
||||
};
|
||||
|
||||
// 4. Verify the signature against the sorted string
|
||||
const isVerified = crypto.verify(
|
||||
"sha256",
|
||||
Buffer.from(signString),
|
||||
publicKey,
|
||||
Buffer.from(sign, 'base64')
|
||||
);
|
||||
|
||||
return isVerified;
|
||||
}
|
||||
|
||||
async handle(payload: TelebirrDto): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { All, Body, Controller, HttpCode, HttpStatus, Logger, } from '@nestjs/common';
|
||||
import { TelebirrWebhookService } from './providers/telebirr.service';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
import { TelebirrDto } from './dto/telebirr.dto';
|
||||
|
||||
@Controller("payments/webhooks")
|
||||
export class WebhookController {
|
||||
constructor(private readonly telebirr: TelebirrWebhookService) { }
|
||||
private readonly logger = new Logger(WebhookController.name);
|
||||
|
||||
@All('telebirr')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Telebirr payment notification callback (Ethiopia)',
|
||||
description: 'Webhook endpoint for Telebirr payment status updates. Used by Ethiopian passengers.'
|
||||
})
|
||||
async receiveTelebirr(@Body() payload: TelebirrDto) {
|
||||
this.logger.log(
|
||||
`Telebirr webhook Called`,
|
||||
);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this.logger.error(`Telebirr webhook handler threw: ${message}`);
|
||||
}
|
||||
return { code: '0', message: 'OK' };
|
||||
}
|
||||
|
||||
}
|
||||
2092
pnpm-lock.yaml
generated
2092
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user