From 93020b3153b63d5f65bb0bee19d7b21c729fd35e Mon Sep 17 00:00:00 2001 From: Tria Date: Thu, 4 Jun 2026 16:18:14 +0300 Subject: [PATCH] working on payment --- apps/edr-freight-api/src/app.module.ts | 2 + .../payment/entities/payment.entity.ts | 59 ++++ .../src/modules/payment/payment.controller.ts | 27 ++ .../src/modules/payment/payment.module.ts | 12 + .../src/modules/payment/payment.service.ts | 42 +++ .../payment/strategies/payment.strategy.ts | 9 + .../strategies/payment.telebirr.strategy.ts | 307 ++++++++++++++++++ .../payment/strategies/payments.types.ts | 39 +++ .../strategies/telebirr/telebirr.crypto.ts | 98 ++++++ .../strategies/telebirr/telebirr.types.ts | 69 ++++ 10 files changed, 664 insertions(+) create mode 100644 apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts create mode 100644 apps/edr-freight-api/src/modules/payment/payment.controller.ts create mode 100644 apps/edr-freight-api/src/modules/payment/payment.module.ts create mode 100644 apps/edr-freight-api/src/modules/payment/payment.service.ts create mode 100644 apps/edr-freight-api/src/modules/payment/strategies/payment.strategy.ts create mode 100644 apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts create mode 100644 apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts create mode 100644 apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.crypto.ts create mode 100644 apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.types.ts diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index a4ecf321e..1f9e7a5ce 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -30,6 +30,7 @@ import { } from "./seed/edr-freight.seed"; import { EdrOrgSeeder } from "./seed/edr-org.seeder"; import { DemoUsersSeeder } from "./seed/demo-users.seeder"; +import { PaymentModule } from "./modules/payment/payment.module"; @Module({ imports: [ @@ -70,6 +71,7 @@ import { DemoUsersSeeder } from "./seed/demo-users.seeder"; RuleEngineModule, BackofficeModule, DemoPermissionsModule, + PaymentModule ], providers: [EdrOrgSeeder, DemoUsersSeeder], }) 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 new file mode 100644 index 000000000..4858aa979 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts @@ -0,0 +1,59 @@ +import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; + + +type PaymentType = "booking" +type PaymentMethod = "telebirr" | "cbe-birr" | "ebirr" +type Currency = "etb" | "usd" +type PaymentStatus = "action-required" | "processing" | "success" | "failed" | "canceled" | "refunded" + +@Entity({ schema: 'freight', name: 'payments' }) +export class PaymentEntity extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + id!: string + + @Column({ type: 'varchar', length: 255, name: "ref_id" }) + refId!: string + + @Column({ type: 'varchar', length: 255, name: "table_id" }) + tableId!: string + + @Column({ type: "enum", enum: ["booking"] }) + type!: PaymentType; + + @Column({ type: "enum", enum: ["telebirr", "cbe-birr", "ebirr"] }) + method!: PaymentMethod + + @Column({ type: "enum", enum: ["etb", "usd"] }) + currency!: Currency + + @Column({ type: "numeric" }) + amount!: number + + @Column({ type: "json", name: "client_action" }) + clientAction?: string; + + @Column({ type: "varchar", length: 255, unique: true, name: "merchant_order_id", }) + merchantOrderId?: string + + @Column({ type: "varchar", length: 255, unique: true, name: "transaction_id", }) + transactionId?: string + + @Column({ type: "enum", enum: ["action-required", "processing", "success", "failed", "canceled", "refunded"], default: "action-required" }) + status!: PaymentStatus + + @Column({ type: "date", nullable: true, name: "paid_at" }) + paidAt?: Date + + @Column({ type: "date", nullable: true, name: "refunded_at" }) + refundedAt?: Date + + @Column({ type: "varchar", length: 30, nullable: true, name: "failer_code" }) + failerCode?: string + + @Column({ type: "varchar", length: 255, nullable: true, name: "failer_message" }) + failureMessage?: string + + @CreateDateColumn({ name: "created_at" }) + createdAt!: Date + +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts new file mode 100644 index 000000000..7cd453f4c --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Post } from "@nestjs/common"; +import { PaymentService } from "./payment.service"; +import { randomUUID } from "crypto"; + +@Controller("payments") +export class PaymentController { + constructor(private readonly paymentService: PaymentService) { } + + @Post("/initiate") + async initiatePayment() { + const tableId = randomUUID() + const resp = await this.paymentService.create({ + amount: 20, + currency: "etb", + method: "telebirr", + type: "booking", + tableId: tableId + }) + return resp + } + + @Post("web-hooks/telebirr") + paymentWebhook() { + + } + +} \ 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 new file mode 100644 index 000000000..120effaf7 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/payment.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { PaymentTelebirrStrategy } from "./strategies/payment.telebirr.strategy"; +import { PaymentService } from "./payment.service"; +import { HttpModule } from "@nestjs/axios"; +import { PaymentController } from "./payment.controller"; + +@Module({ + imports: [HttpModule,], + providers: [PaymentTelebirrStrategy, PaymentService], + controllers: [PaymentController] +}) +export class PaymentModule { } \ 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 new file mode 100644 index 000000000..1dab7ffd7 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -0,0 +1,42 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { DataSource, Repository } from "typeorm"; +import { PaymentEntity } from "./entities/payment.entity"; +import { PaymentStrategy } from "./strategies/payment.strategy"; +import { PaymentTelebirrStrategy } from "./strategies/payment.telebirr.strategy"; + +type PaymentMethod = PaymentEntity["method"] +@Injectable() +export class PaymentService { + private strategies: Map; + private readonly paymentRepo: Repository; + + constructor( + private readonly dataSource: DataSource, + private readonly telebirrPaymentStategy: PaymentTelebirrStrategy) { + this.strategies = new Map([ + ["telebirr", this.telebirrPaymentStategy as PaymentStrategy] + ]) + this.paymentRepo = this.dataSource.getRepository(PaymentEntity) + } + + async create(data: Pick): Promise { + const strategy = this.strategies.get(data.method) + if (!strategy) throw new NotFoundException("strategy not found") + return strategy.pay(data.amount, data.currency, "") + // const payment = this.paymentRepo.create({ + // amount: data.amount, + // method: data.method, + // type: data.type, + // currency: data.currency, + // tableId: data.tableId, + // }) + // await this.paymentRepo.save(payment) + // return link; + } + + getPaymentByReferenceIdAndMethod(data: Pick): Promise { + return this.paymentRepo.findOneBy(data) + } + + +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/strategies/payment.strategy.ts b/apps/edr-freight-api/src/modules/payment/strategies/payment.strategy.ts new file mode 100644 index 000000000..a74fe9782 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/strategies/payment.strategy.ts @@ -0,0 +1,9 @@ +import { Injectable } from "@nestjs/common"; +import { PaymentEntity } from "../entities/payment.entity"; + +type PaymentCurrency = PaymentEntity["currency"] + +@Injectable() +export abstract class PaymentStrategy { + abstract pay(amount: number, currency: PaymentCurrency, refId: string): Promise +} \ 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 new file mode 100644 index 000000000..521b6ee9b --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts @@ -0,0 +1,307 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PaymentStrategy } from "./payment.strategy"; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { firstValueFrom } from 'rxjs'; +import * as https from 'node:https'; +import { PaymentEntity } from "../entities/payment.entity"; +import { ProviderInitiationInput, ProviderInitiationResult, ProviderStatus } from "./payments.types"; +import { CreateOrderRequest, CreateOrderResponse, FabricTokenResponse, QueryOrderResponse } from "./telebirr/telebirr.types"; +import { createNonceStr, createTimestamp, signRequestObject, verifyRequestObject } from "./telebirr/telebirr.crypto"; +import { randomUUID } from "node:crypto"; + + + +// type PaymentCurrency = PaymentEntity["currency"] +type PaymentIntentStatus = PaymentEntity["status"] +const TELEBIRR_HTTP_TIMEOUT_MS = 10_000; + +@Injectable() +export class PaymentTelebirrStrategy implements PaymentStrategy { + async pay(amount: number): Promise { + const refId = randomUUID() + const resp = await this.initiate({ + amountMinor: amount, + bookingRef: refId, + currency: "etb", + merchantOrderId: "", + platform: "web" + }) + return resp; + } + + // readonly method = PaymentMethodType.TELEBIRR; + private readonly logger = new Logger(PaymentTelebirrStrategy.name); + private readonly httpsAgent: https.Agent; + + constructor( + private readonly config: ConfigService, + private readonly http: HttpService, + ) { + const insecure = this.config.get('telebirr.insecureTls'); + if (insecure) { + this.logger.warn('TELEBIRR_INSECURE_TLS=true — TLS verification disabled for Telebirr calls. DEV ONLY.'); + } + this.httpsAgent = new https.Agent({ + rejectUnauthorized: !insecure, + secureProtocol: 'TLSv1_2_method', + }); + } + + async initiate(input: ProviderInitiationInput): Promise { + const fabricToken = await this.applyFabricToken(); + const requestBody = this.buildCreateOrderRequest(input); + const response = await this.requestCreateOrder(fabricToken, requestBody); + + const prepayId = response.biz_content?.prepay_id; + if (!prepayId) { + throw new Error( + `Telebirr createOrder returned no prepay_id: ${JSON.stringify(response)}`, + ); + } + + const expiresAt = this.computeExpiresAt(requestBody.biz_content.timeout_express); + const platform = input.platform ?? 'web'; + const clientAction = + platform === 'mobile' + ? { + type: 'LAUNCH_APP' as const, + prepayId, + receiveCode: response.biz_content?.receiveCode, + shortCode: this.merchantCode, + } + : { type: 'REDIRECT' as const, url: this.buildCheckoutUrl(prepayId) }; + + return { + providerOrderId: prepayId, + clientAction, + expiresAt, + rawInitiation: { + request: this.sanitize(requestBody), + response, + }, + }; + } + + async queryStatus(merchantOrderId: string): Promise { + const fabricToken = await this.applyFabricToken(); + const requestBody = this.buildQueryOrderRequest(merchantOrderId); + const response = await this.postJson( + `${this.baseUrl}/payment/v1/merchant/queryOrder`, + requestBody, + { + 'Content-Type': 'application/json', + 'X-APP-Key': this.fabricAppId, + Authorization: fabricToken, + }, + ); + + const tradeStatus = response.biz_content?.trade_status; + const providerTxnId = + response.biz_content?.trans_id ?? response.biz_content?.payment_order_id; + const mapped = this.mapTradeStatus(tradeStatus); + + return { + status: mapped, + providerTxnId, + failureCode: + mapped === "failed" && tradeStatus ? tradeStatus : undefined, + rawResponse: response as Record, + }; + } + + mapTradeStatus(tradeStatus: string | undefined): PaymentIntentStatus { + switch (tradeStatus) { + case 'PAY_SUCCESS': + return "success"; + case 'PAY_FAILED': + case 'ORDER_CLOSED': + return "failed"; + case 'WAIT_PAY': + return "action-required"; + case 'PAYING': + return "processing"; + default: + return "processing"; + } + } + + mapWebhookTradeStatus(tradeStatus: string | undefined): PaymentIntentStatus { + switch (tradeStatus) { + case 'Completed': + return "success"; + case 'Failure': + case 'Expired': + return "failed"; + case 'Paying': + case 'Pending': + return "processing"; + default: + return "processing"; + } + } + + verifyWebhookSignature(payload: Record): boolean { + if (!this.publicKey) { + this.logger.error('TELEBIRR_PUBLIC_KEY not configured; rejecting all webhooks'); + return false; + } + return verifyRequestObject(payload, this.publicKey); + } + + private async applyFabricToken(): Promise { + const response = await this.postJson( + `${this.baseUrl}/payment/v1/token`, + { appSecret: this.appSecret }, + { + 'Content-Type': 'application/json', + 'X-APP-Key': this.fabricAppId, + }, + ); + if (!response?.token) { + throw new Error(`Telebirr token request failed: ${JSON.stringify(response)}`); + } + return response.token; + } + + private async requestCreateOrder( + fabricToken: string, + body: CreateOrderRequest, + ): Promise { + return this.postJson( + `${this.baseUrl}/payment/v1/inapp/createOrder`, + body, + { + 'Content-Type': 'application/json', + 'X-APP-Key': this.fabricAppId, + Authorization: fabricToken, + }, + ); + } + + private buildCreateOrderRequest(input: ProviderInitiationInput): CreateOrderRequest { + const totalAmount = String(input.amountMinor / 100); + const req = { + timestamp: createTimestamp(), + nonce_str: createNonceStr(), + method: 'payment.preorder' as const, + version: '1.0' as const, + biz_content: { + notify_url: this.notifyUrl, + appid: this.merchantAppId, + merch_code: this.merchantCode, + merch_order_id: input.merchantOrderId, + trade_type: 'Checkout' as const, + title: `EDR Booking`, + total_amount: totalAmount, + trans_currency: input.currency, + timeout_express: this.timeoutExpress, + }, + }; + const sign = signRequestObject(req as unknown as Record, this.privateKey); + return { ...req, sign, sign_type: 'SHA256WithRSA' }; + } + + private buildQueryOrderRequest(merchantOrderId: string): Record { + const req = { + timestamp: createTimestamp(), + nonce_str: createNonceStr(), + method: 'payment.queryorder', + version: '1.0', + biz_content: { + appid: this.merchantAppId, + merch_code: this.merchantCode, + merch_order_id: merchantOrderId, + }, + }; + const sign = signRequestObject(req as Record, this.privateKey); + return { ...req, sign, sign_type: 'SHA256WithRSA' }; + } + + private buildCheckoutUrl(prepayId: string): string { + const map: Record = { + appid: this.merchantAppId, + merch_code: this.merchantCode, + nonce_str: createNonceStr(), + prepay_id: prepayId, + timestamp: createTimestamp(), + }; + const sign = signRequestObject(map, this.privateKey); + const rawRequest = [ + `appid=${map.appid}`, + `merch_code=${map.merch_code}`, + `nonce_str=${map.nonce_str}`, + `prepay_id=${map.prepay_id}`, + `timestamp=${map.timestamp}`, + 'sign_type=SHA256WithRSA', + `sign=${sign}`, + 'version=1.0', + 'trade_type=Checkout', + ].join('&'); + return `${this.webBaseUrl}${rawRequest}`; + } + + private computeExpiresAt(timeoutExpress: string): Date { + const match = /^(\d+)([smhd])$/.exec(timeoutExpress); + const minutes = match ? this.toMinutes(parseInt(match[1], 10), match[2]) : 15; + return new Date(Date.now() + minutes * 60_000); + } + + private toMinutes(n: number, unit: string): number { + switch (unit) { + case 's': return Math.max(1, Math.round(n / 60)); + case 'm': return n; + case 'h': return n * 60; + case 'd': return n * 60 * 24; + default: return 15; + } + } + + private async postJson( + url: string, + body: unknown, + headers: Record, + ): Promise { + const config: AxiosRequestConfig = { + headers, + timeout: TELEBIRR_HTTP_TIMEOUT_MS, + httpsAgent: this.httpsAgent, + }; + const started = Date.now(); + try { + const res = await firstValueFrom(this.http.post(url, body, config)); + this.logger.debug(`Telebirr POST ${url} status=${res.status} latency=${Date.now() - started}ms`); + return res.data; + } catch (err) { + if (err instanceof AxiosError) { + this.logger.error( + `Telebirr POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)} code=${err.code} message=${err.message}`, + ); + } else { + this.logger.error(`Telebirr POST ${url} threw: ${err instanceof Error ? err.message : err}`); + } + throw err; + } + } + + private sanitize(body: CreateOrderRequest): Record { + const { sign: _sign, ...rest } = body; + return rest; + } + + private get baseUrl(): string { return this.config.get('telebirr.baseUrl') ?? ''; } + private get webBaseUrl(): string { return this.config.get('telebirr.webBaseUrl') ?? ''; } + private get fabricAppId(): string { return this.config.get('telebirr.fabricAppId') ?? ''; } + private get appSecret(): string { return this.config.get('telebirr.appSecret') ?? ''; } + private get merchantAppId(): string { return this.config.get('telebirr.merchantAppId') ?? ''; } + private get merchantCode(): string { return this.config.get('telebirr.merchantCode') ?? ''; } + private get notifyUrl(): string { return this.config.get('telebirr.notifyUrl') ?? ''; } + private get timeoutExpress(): string { return this.config.get('telebirr.timeoutExpress') ?? '15m'; } + private get privateKey(): string { return this.config.get('telebirr.privateKey') ?? ''; } + private get publicKey(): string { + return this.config.get('telebirr.publicKey') ?? ''; + } + + +} \ No newline at end of file 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 new file mode 100644 index 000000000..bf803c3e2 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts @@ -0,0 +1,39 @@ +import { PaymentEntity } from "../entities/payment.entity"; + +type PaymentIntentStatus = PaymentEntity["status"] +type PaymentMethodType = PaymentEntity["method"] + +export type PaymentPlatform = 'web' | 'mobile'; + +export type ClientAction = + | { type: 'REDIRECT'; url: string } + | { type: 'LAUNCH_APP'; prepayId: string; receiveCode?: string; shortCode: string }; + +export interface ProviderInitiationInput { + merchantOrderId: string; + bookingRef: string; + amountMinor: number; + currency: string; + platform?: PaymentPlatform; +} + +export interface ProviderInitiationResult { + providerOrderId: string; + clientAction: ClientAction; + expiresAt: Date; + rawInitiation: Record; +} + +export interface ProviderStatus { + status: PaymentIntentStatus; + providerTxnId?: string; + failureCode?: string; + failureMessage?: string; + rawResponse: Record; +} + +export interface PaymentProvider { + readonly method: PaymentMethodType; + initiate(input: ProviderInitiationInput): Promise; + queryStatus(merchantOrderId: string): Promise; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.crypto.ts b/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.crypto.ts new file mode 100644 index 000000000..20319818d --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.crypto.ts @@ -0,0 +1,98 @@ +import * as crypto from 'crypto'; + +const EXCLUDE_FIELDS = new Set([ + 'sign', + 'sign_type', + 'header', + 'refund_info', + 'openType', + 'raw_request', + 'biz_content', +]); + +const NONCE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + +export function buildCanonicalString(requestObject: Record): string { + const fieldMap: Record = {}; + + for (const key of Object.keys(requestObject)) { + if (EXCLUDE_FIELDS.has(key)) continue; + fieldMap[key] = requestObject[key]; + } + + const biz = requestObject['biz_content']; + if (biz && typeof biz === 'object') { + for (const key of Object.keys(biz as Record)) { + if (EXCLUDE_FIELDS.has(key)) continue; + fieldMap[key] = (biz as Record)[key]; + } + } + + return Object.keys(fieldMap) + .sort() + .map((k) => `${k}=${fieldMap[k]}`) + .join('&'); +} + +export function signRequestObject( + requestObject: Record, + privateKey: string, +): string { + return signString(buildCanonicalString(requestObject), privateKey); +} + +export function verifyRequestObject( + requestObject: Record, + publicKey: string, +): boolean { + const signature = requestObject['sign']; + if (typeof signature !== 'string' || signature.length === 0) return false; + return verifySignature(buildCanonicalString(requestObject), signature, publicKey); +} + +export function signString(text: string, privateKey: string): string { + const signature = crypto.sign('sha256', Buffer.from(text), { + key: privateKey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, + }); + return signature.toString('base64'); +} + +export function verifySignature( + text: string, + signatureBase64: string, + publicKey: string, +): boolean { + try { + return crypto.verify( + 'sha256', + Buffer.from(text), + { + key: publicKey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, + }, + Buffer.from(signatureBase64, 'base64'), + ); + } catch { + return false; + } +} + +export function createTimestamp(): string { + return Math.round(Date.now() / 1000).toString(); +} + +export function createNonceStr(length = 32): string { + const bytes = crypto.randomBytes(length); + let out = ''; + for (let i = 0; i < length; i++) { + out += NONCE_CHARS[bytes[i] % NONCE_CHARS.length]; + } + return out; +} + +export function createMerchantOrderId(): string { + return `${Date.now()}${crypto.randomBytes(4).toString('hex')}`; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.types.ts b/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.types.ts new file mode 100644 index 000000000..6cc29e9f4 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/strategies/telebirr/telebirr.types.ts @@ -0,0 +1,69 @@ +export interface FabricTokenResponse { + token: string; + expires_in?: number | string; +} + +export interface CreateOrderBizContent { + notify_url: string; + appid: string; + merch_code: string; + merch_order_id: string; + trade_type: 'Checkout' | 'InApp' | 'MiniApp'; + title: string; + total_amount: string; + trans_currency: string; + timeout_express: string; +} + +export interface CreateOrderRequest { + timestamp: string; + nonce_str: string; + method: 'payment.preorder'; + version: '1.0'; + biz_content: CreateOrderBizContent; + sign: string; + sign_type: 'SHA256WithRSA'; +} + +export interface CreateOrderResponse { + code?: string; + msg?: string; + biz_content?: { + prepay_id?: string; + receiveCode?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export type TelebirrTradeStatus = + | 'PAY_SUCCESS' + | 'PAY_FAILED' + | 'WAIT_PAY' + | 'ORDER_CLOSED' + | 'PAYING' + | 'ACCEPTED' + | 'REFUNDING' + | 'REFUND_SUCCESS' + | 'REFUND_FAILED'; + +export interface QueryOrderResponse { + result?: 'SUCCESS' | 'FAIL'; + code?: string; + msg?: string; + nonce_str?: string; + sign?: string; + sign_type?: string; + biz_content?: { + merch_order_id?: string; + order_status?: string; + trade_status?: TelebirrTradeStatus | string; + payment_order_id?: string; + trans_id?: string; + trans_time?: string; + trans_currency?: string; + total_amount?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} \ No newline at end of file