mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: ( payment ) implement d-money payment
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "PaymentMethodType" ADD VALUE 'DMONEY';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,8 @@ export enum PaymentMethodTypeEnum {
|
||||
TELEBIRR = "TELEBIRR", // Ethiopia
|
||||
CBE_BIRR = "CBE_BIRR", // Ethiopia
|
||||
EBIRR = "EBIRR", // Ethiopia
|
||||
WAAFI = "WAAFI", // Djibouti
|
||||
WAAFI = "WAAFI",
|
||||
DMONEY= "DMONEY",// Djibouti
|
||||
CARD = "CARD", // International
|
||||
WALLET = "WALLET", // Internal
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@ import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("dmoney", () => ({
|
||||
baseUrl: process.env.DMONEY_BASE_URL ?? "",
|
||||
appId: process.env.DMONEY_APP_ID ?? "",
|
||||
webBaseUrl: process.env.DMONEY_WEB_BASE_URL ?? "",
|
||||
fabricAppId: process.env.DMONEY_FABRIC_APP_ID ?? "",
|
||||
appSecret: process.env.DMONEY_APP_SECRET ?? "",
|
||||
publicKey: process.env.DMONEY_PUBLIC_KEY ?? "",
|
||||
privateKey: process.env.DMONEY_PRIVATE_KEY ?? "",
|
||||
merchantAppId: process.env.DMONEY_MERCHANT_APP_ID ?? "",
|
||||
merchantCode: process.env.DMONEY_MERCHANT_CODE ?? "",
|
||||
notifyUrl: process.env.DMONEY_NOTIFY_URL ?? "",
|
||||
returnUrl: process.env.DMONEY_RETURN_URL ?? "",
|
||||
timeoutExpress: process.env.DMONEY_TIMEOUT_EXPRESS ?? "120m",
|
||||
language: process.env.DMONEY_LANGUAGE ?? "en",
|
||||
currency: process.env.DMONEY_CURRENCY ?? "FDJ",
|
||||
privateKey: process.env.DMONEY_PRIVATE_KEY ?? "",
|
||||
publicKey: process.env.DMONEY_PUBLIC_KEY ?? "",
|
||||
insecureTls: process.env.DMONEY_INSECURE_TLS === "true",
|
||||
}));
|
||||
|
||||
@@ -13,22 +13,36 @@ export class DMoneyWebhookService {
|
||||
const signatureValid = this.provider.verifyWebhookSignature(
|
||||
payload as unknown as Record<string, unknown>,
|
||||
);
|
||||
const mapped = this.provider.mapWebhookStatus(payload.status);
|
||||
const mapped = this.provider.mapWebhookTradeStatus(payload.trade_status);
|
||||
const providerTxnId = payload.transId ?? payload.payment_order_id;
|
||||
|
||||
await this.processor.process({
|
||||
provider: this.provider.method,
|
||||
externalEventId: `${payload.orderId}_${payload.status}`,
|
||||
merchantOrderId: payload.merchantOrderId,
|
||||
providerTxnId: payload.transactionId,
|
||||
externalEventId: `${payload.payment_order_id}_${payload.trade_status}`,
|
||||
merchantOrderId: payload.merch_order_id,
|
||||
providerTxnId,
|
||||
signatureValid,
|
||||
rawStatus: payload.status,
|
||||
rawStatus: payload.trade_status,
|
||||
payload: payload as unknown as Record<string, unknown>,
|
||||
result: {
|
||||
status: mapped,
|
||||
providerTxnId: payload.transactionId,
|
||||
paidAt: payload.paidAt ? new Date(payload.paidAt) : undefined,
|
||||
failureCode: payload.status,
|
||||
providerTxnId,
|
||||
paidAt: this.parseTransEndTime(payload.trans_end_time),
|
||||
failureCode: payload.trade_status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** D-Money sends trans_end_time either as epoch ms/s or "YYYY-MM-DD HH:mm:ss". */
|
||||
private parseTransEndTime(raw: string | undefined): Date | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (/^\d+$/.test(raw)) {
|
||||
const n = parseInt(raw, 10);
|
||||
if (Number.isNaN(n)) return undefined;
|
||||
// 13-digit value is milliseconds, otherwise seconds.
|
||||
return new Date(raw.length >= 13 ? n : n * 1000);
|
||||
}
|
||||
const parsed = new Date(raw.replace(" ", "T"));
|
||||
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export class WebhooksController {
|
||||
} catch (err) {
|
||||
this.logger.error(`D-Money webhook handler threw: ${this.message(err)}`);
|
||||
}
|
||||
return { success: true };
|
||||
return { code: "0", msg: "Success", result: "SUCCESS" };
|
||||
}
|
||||
|
||||
private message(err: unknown): string {
|
||||
|
||||
@@ -40,6 +40,16 @@ export type {
|
||||
TelebirrTradeStatus,
|
||||
} from './providers/telebirr/telebirr.types';
|
||||
|
||||
// D-Money request/response types (exported for apps that build/inspect requests directly)
|
||||
export type {
|
||||
DMoneyFabricTokenResponse,
|
||||
DMoneyPreOrderBizContent,
|
||||
DMoneyPreOrderRequest,
|
||||
DMoneyPreOrderResponse,
|
||||
DMoneyQueryOrderResponse,
|
||||
DMoneyOrderStatus,
|
||||
} from './providers/dmoney/dmoney.types';
|
||||
|
||||
// Waafi HPP request/response types (exported for apps that build/inspect requests directly)
|
||||
export type {
|
||||
WaafiState,
|
||||
|
||||
@@ -11,97 +11,83 @@ import {
|
||||
} from "@edr/types";
|
||||
import { AxiosError, AxiosRequestConfig } from "axios";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as https from "node:https";
|
||||
import {
|
||||
createNonceStr,
|
||||
createTimestamp,
|
||||
signRequestObject,
|
||||
verifyRequestObject,
|
||||
} from "../telebirr/telebirr.crypto";
|
||||
import {
|
||||
DMoneyFabricTokenResponse,
|
||||
DMoneyPreOrderRequest,
|
||||
DMoneyPreOrderResponse,
|
||||
DMoneyQueryOrderResponse,
|
||||
} from "./dmoney.types";
|
||||
|
||||
interface DMoneyAuthResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface DMoneyInitiateRequest {
|
||||
merchantId: string;
|
||||
merchantOrderId: string;
|
||||
amount: string;
|
||||
currency: string;
|
||||
description: string;
|
||||
returnUrl: string;
|
||||
notifyUrl: string;
|
||||
payerPhone?: string;
|
||||
timestamp: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
interface DMoneyInitiateResponse {
|
||||
success: boolean;
|
||||
orderId: string;
|
||||
checkoutUrl?: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
interface DMoneyQueryResponse {
|
||||
success: boolean;
|
||||
orderId: string;
|
||||
status: string;
|
||||
transactionId?: string;
|
||||
amount?: string;
|
||||
currency?: string;
|
||||
paidAt?: string;
|
||||
payerPhone?: string;
|
||||
}
|
||||
const DMONEY_HTTP_TIMEOUT_MS = 10_000;
|
||||
|
||||
/**
|
||||
* D-Money (Djibouti) shares the same payment-gateway platform as Telebirr: fabric-token auth,
|
||||
* payment.preorder / payment.queryorder, SHA256withRSA (PSS) signing, and a signed paygate
|
||||
* web-checkout redirect. This provider mirrors TelebirrProvider, differing only in endpoint
|
||||
* paths, the already-"Bearer"-prefixed token, the queryOrder status field (order_status), and
|
||||
* the web-only client action (no LAUNCH_APP). Crypto is reused from telebirr.crypto (RSA-PSS).
|
||||
*/
|
||||
@Injectable()
|
||||
export class DMoneyProvider implements PaymentProvider {
|
||||
readonly method = ProviderMethod.DMONEY;
|
||||
private readonly logger = new Logger(DMoneyProvider.name);
|
||||
private readonly httpsAgent: https.Agent;
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly http: HttpService,
|
||||
) {}
|
||||
) {
|
||||
const insecure = this.config.get<boolean>("dmoney.insecureTls");
|
||||
if (insecure) {
|
||||
this.logger.warn(
|
||||
"DMONEY_INSECURE_TLS=true — TLS verification disabled for D-Money calls. DEV ONLY.",
|
||||
);
|
||||
}
|
||||
this.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: !insecure,
|
||||
secureProtocol: "TLSv1_2_method",
|
||||
});
|
||||
}
|
||||
|
||||
async initiate(
|
||||
input: ProviderInitiationInput,
|
||||
): Promise<ProviderInitiationResult> {
|
||||
const token = await this.getFabricToken();
|
||||
const amount = (input.amountMinor / 100).toFixed(2);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const requestBody: DMoneyInitiateRequest = {
|
||||
merchantId: this.merchantId,
|
||||
merchantOrderId: input.merchantOrderId,
|
||||
amount,
|
||||
currency: input.currency,
|
||||
description: `EDR ${input.orderRef}`,
|
||||
returnUrl: this.returnUrl,
|
||||
notifyUrl: this.notifyUrl,
|
||||
timestamp,
|
||||
signature: this.signRequest({
|
||||
merchantId: this.merchantId,
|
||||
merchantOrderId: input.merchantOrderId,
|
||||
amount,
|
||||
timestamp,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await this.postJson<DMoneyInitiateResponse>(
|
||||
`${this.baseUrl}/api/v1/payment/initiate`,
|
||||
const fabricToken = await this.applyFabricToken();
|
||||
const requestBody = this.buildPreOrderRequest(input);
|
||||
const response = await this.postJson<DMoneyPreOrderResponse>(
|
||||
`${this.baseUrl}/apiaccess/payment/gateway/payment/v1/merchant/preOrder`,
|
||||
requestBody,
|
||||
token,
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"X-APP-Key": this.fabricAppId,
|
||||
Authorization: fabricToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.success || !response.orderId) {
|
||||
throw new Error(`DMoney initiate failed: ${JSON.stringify(response)}`);
|
||||
const prepayId = response.biz_content?.prepay_id;
|
||||
if (response.result !== "SUCCESS" || !prepayId) {
|
||||
throw new Error(
|
||||
`D-Money preOrder failed: ${JSON.stringify(response)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + response.expiresIn * 1000);
|
||||
const expiresAt = this.computeExpiresAt(
|
||||
requestBody.biz_content.timeout_express,
|
||||
);
|
||||
|
||||
return {
|
||||
providerOrderId: response.orderId,
|
||||
clientAction: response.checkoutUrl
|
||||
? { type: "REDIRECT", url: response.checkoutUrl }
|
||||
: {
|
||||
type: "REDIRECT",
|
||||
url: `${this.baseUrl}/checkout/${response.orderId}`,
|
||||
},
|
||||
providerOrderId: prepayId,
|
||||
clientAction: {
|
||||
type: "REDIRECT",
|
||||
url: this.buildCheckoutUrl(prepayId),
|
||||
},
|
||||
expiresAt,
|
||||
rawInitiation: {
|
||||
request: this.sanitize(requestBody),
|
||||
@@ -111,151 +97,239 @@ export class DMoneyProvider implements PaymentProvider {
|
||||
}
|
||||
|
||||
async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
|
||||
const token = await this.getFabricToken();
|
||||
const timestamp = new Date().toISOString();
|
||||
const signature = this.signRequest({
|
||||
merchantId: this.merchantId,
|
||||
merchantOrderId,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const response = await this.postJson<DMoneyQueryResponse>(
|
||||
`${this.baseUrl}/api/v1/payment/query`,
|
||||
const fabricToken = await this.applyFabricToken();
|
||||
const requestBody = this.buildQueryOrderRequest(merchantOrderId);
|
||||
const response = await this.postJson<DMoneyQueryOrderResponse>(
|
||||
`${this.baseUrl}/apiaccess/payment/v1/merchant/queryOrder`,
|
||||
requestBody,
|
||||
{
|
||||
merchantId: this.merchantId,
|
||||
merchantOrderId,
|
||||
timestamp,
|
||||
signature,
|
||||
"Content-Type": "application/json",
|
||||
"X-APP-Key": this.fabricAppId,
|
||||
Authorization: fabricToken,
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
||||
const mapped = this.mapStatus(response.status);
|
||||
const orderStatus = response.biz_content?.order_status;
|
||||
const providerTxnId = response.biz_content?.payment_order_id;
|
||||
const mapped = this.mapOrderStatus(orderStatus);
|
||||
|
||||
return {
|
||||
status: mapped,
|
||||
providerTxnId: response.transactionId,
|
||||
providerTxnId,
|
||||
failureCode:
|
||||
mapped === ProviderPaymentStatus.FAILED ? response.status : undefined,
|
||||
rawResponse: response as unknown as Record<string, unknown>,
|
||||
mapped === ProviderPaymentStatus.FAILED && orderStatus
|
||||
? orderStatus
|
||||
: undefined,
|
||||
rawResponse: response as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
verifyWebhookSignature(payload: Record<string, unknown>): boolean {
|
||||
const { signature, ...data } = payload;
|
||||
if (!signature || typeof signature !== "string") return false;
|
||||
|
||||
const expectedSignature = this.signRequest(data);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature),
|
||||
);
|
||||
}
|
||||
|
||||
mapWebhookStatus(status: string): ProviderPaymentStatus {
|
||||
return this.mapStatus(status);
|
||||
}
|
||||
|
||||
private mapStatus(status: string): ProviderPaymentStatus {
|
||||
switch (status?.toUpperCase()) {
|
||||
/** queryOrder `order_status` → shared status. */
|
||||
mapOrderStatus(orderStatus: string | undefined): ProviderPaymentStatus {
|
||||
switch (orderStatus) {
|
||||
case "PAY_SUCCESS":
|
||||
case "Completed":
|
||||
case "SUCCESS":
|
||||
case "COMPLETED":
|
||||
return ProviderPaymentStatus.SUCCEEDED;
|
||||
case "FAILED":
|
||||
case "REJECTED":
|
||||
case "EXPIRED":
|
||||
case "CANCELLED":
|
||||
case "PAY_FAILED":
|
||||
case "Failure":
|
||||
case "ORDER_CLOSED":
|
||||
case "Expired":
|
||||
return ProviderPaymentStatus.FAILED;
|
||||
case "PENDING":
|
||||
case "WAIT_PAY":
|
||||
return ProviderPaymentStatus.REQUIRES_ACTION;
|
||||
case "PROCESSING":
|
||||
case "PAYING":
|
||||
case "Paying":
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
default:
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
}
|
||||
}
|
||||
|
||||
private async getFabricToken(): Promise<string> {
|
||||
const response = await this.postJson<DMoneyAuthResponse>(
|
||||
/** Notification `trade_status` → shared status. */
|
||||
mapWebhookTradeStatus(
|
||||
tradeStatus: string | undefined,
|
||||
): ProviderPaymentStatus {
|
||||
switch (tradeStatus) {
|
||||
case "Completed":
|
||||
return ProviderPaymentStatus.SUCCEEDED;
|
||||
case "Failure":
|
||||
case "Expired":
|
||||
return ProviderPaymentStatus.FAILED;
|
||||
case "Paying":
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
default:
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
}
|
||||
}
|
||||
|
||||
verifyWebhookSignature(payload: Record<string, unknown>): boolean {
|
||||
if (!this.publicKey) {
|
||||
this.logger.error(
|
||||
"DMONEY_PUBLIC_KEY not configured; rejecting all webhooks",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return verifyRequestObject(payload, this.publicKey);
|
||||
}
|
||||
|
||||
private async applyFabricToken(): Promise<string> {
|
||||
const response = await this.postJson<DMoneyFabricTokenResponse>(
|
||||
`${this.baseUrl}/apiaccess/payment/gateway/payment/v1/token`,
|
||||
{ appSecret: this.appSecret },
|
||||
{
|
||||
appSecret: this.appSecret,
|
||||
"Content-Type": "application/json",
|
||||
"X-APP-Key": this.fabricAppId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.token) {
|
||||
if (!response?.token) {
|
||||
throw new Error(
|
||||
`DMoney authentication failed: ${JSON.stringify(response)}`,
|
||||
`D-Money token request failed: ${JSON.stringify(response)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// D-Money returns the token already prefixed with "Bearer " — use it verbatim.
|
||||
return response.token;
|
||||
}
|
||||
|
||||
private signRequest(data: Record<string, unknown>): string {
|
||||
const sortedKeys = Object.keys(data).sort();
|
||||
const signString = sortedKeys.map((key) => `${key}=${data[key]}`).join("&");
|
||||
private buildPreOrderRequest(
|
||||
input: ProviderInitiationInput,
|
||||
): DMoneyPreOrderRequest {
|
||||
const totalAmount = (input.amountMinor / 100).toFixed(2);
|
||||
const redirectUrl = input.redirectUrl ?? this.returnUrl;
|
||||
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 ${input.orderRef}`,
|
||||
total_amount: totalAmount,
|
||||
trans_currency: 1 == 1 ? "DJF": this.currency,
|
||||
timeout_express: this.timeoutExpress,
|
||||
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
return crypto
|
||||
.createHmac("sha256", this.secretKey)
|
||||
.update(signString)
|
||||
.digest("hex");
|
||||
console.log("\n\n\n")
|
||||
console.log(req)
|
||||
console.log("\n\n\n")
|
||||
const sign = signRequestObject(
|
||||
req as unknown as Record<string, unknown>,
|
||||
this.privateKey,
|
||||
);
|
||||
return { ...req, sign, sign_type: "SHA256WithRSA" };
|
||||
}
|
||||
|
||||
private buildQueryOrderRequest(
|
||||
merchantOrderId: string,
|
||||
): Record<string, unknown> {
|
||||
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<string, unknown>,
|
||||
this.privateKey,
|
||||
);
|
||||
return { ...req, sign, sign_type: "SHA256WithRSA" };
|
||||
}
|
||||
|
||||
private buildCheckoutUrl(prepayId: string): string {
|
||||
// Only these five fields are signed for the paygate URL.
|
||||
const map: Record<string, string> = {
|
||||
appid: this.merchantAppId,
|
||||
merch_code: this.merchantCode,
|
||||
nonce_str: createNonceStr(),
|
||||
prepay_id: prepayId,
|
||||
timestamp: createTimestamp(),
|
||||
};
|
||||
const sign = signRequestObject(map, this.privateKey);
|
||||
const query = [
|
||||
`appid=${map.appid}`,
|
||||
`merch_code=${map.merch_code}`,
|
||||
`nonce_str=${map.nonce_str}`,
|
||||
`prepay_id=${map.prepay_id}`,
|
||||
`timestamp=${map.timestamp}`,
|
||||
`sign=${sign}`,
|
||||
"sign_type=SHA256WithRSA",
|
||||
"version=1.0",
|
||||
"trade_type=Checkout",
|
||||
`language=${this.language}`,
|
||||
].join("&");
|
||||
return `${this.webBaseUrl}/payment/web/paygate?${query}`;
|
||||
}
|
||||
|
||||
private computeExpiresAt(timeoutExpress: string): Date {
|
||||
const match = /^(\d+)m$/.exec(timeoutExpress);
|
||||
const minutes = match ? parseInt(match[1], 10) : 120;
|
||||
return new Date(Date.now() + minutes * 60_000);
|
||||
}
|
||||
|
||||
private async postJson<T>(
|
||||
url: string,
|
||||
body: unknown,
|
||||
token?: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
headers,
|
||||
timeout: 10_000,
|
||||
timeout: DMONEY_HTTP_TIMEOUT_MS,
|
||||
httpsAgent: this.httpsAgent,
|
||||
};
|
||||
|
||||
const started = Date.now();
|
||||
try {
|
||||
const res = await firstValueFrom(this.http.post<T>(url, body, config));
|
||||
this.logger.debug(
|
||||
`DMoney POST ${url} status=${res.status} latency=${Date.now() - started}ms`,
|
||||
`D-Money POST ${url} status=${res.status} latency=${Date.now() - started}ms`,
|
||||
);
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
this.logger.error(
|
||||
`DMoney POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)}`,
|
||||
`D-Money POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)} code=${err.code} message=${err.message}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`DMoney POST ${url} threw: ${err instanceof Error ? err.message : err}`,
|
||||
`D-Money POST ${url} threw: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private sanitize(body: DMoneyInitiateRequest): Record<string, unknown> {
|
||||
const { signature: _signature, ...rest } = body;
|
||||
private sanitize(body: DMoneyPreOrderRequest): Record<string, unknown> {
|
||||
const { sign: _sign, ...rest } = body;
|
||||
return rest;
|
||||
}
|
||||
|
||||
private get baseUrl(): string {
|
||||
return this.config.get<string>("dmoney.baseUrl") ?? "";
|
||||
}
|
||||
private get merchantId(): string {
|
||||
return this.config.get<string>("dmoney.merchantId") ?? "";
|
||||
private get webBaseUrl(): string {
|
||||
return this.config.get<string>("dmoney.webBaseUrl") ?? "";
|
||||
}
|
||||
private get fabricAppId(): string {
|
||||
return this.config.get<string>("dmoney.fabricAppId") ?? "";
|
||||
}
|
||||
private get appSecret(): string {
|
||||
return this.config.get<string>("dmoney.appSecret") ?? "";
|
||||
}
|
||||
private get secretKey(): string {
|
||||
return this.config.get<string>("dmoney.secretKey") ?? "";
|
||||
private get merchantAppId(): string {
|
||||
return this.config.get<string>("dmoney.merchantAppId") ?? "";
|
||||
}
|
||||
private get merchantCode(): string {
|
||||
return this.config.get<string>("dmoney.merchantCode") ?? "";
|
||||
}
|
||||
private get notifyUrl(): string {
|
||||
return this.config.get<string>("dmoney.notifyUrl") ?? "";
|
||||
@@ -263,4 +337,19 @@ export class DMoneyProvider implements PaymentProvider {
|
||||
private get returnUrl(): string {
|
||||
return this.config.get<string>("dmoney.returnUrl") ?? "";
|
||||
}
|
||||
private get timeoutExpress(): string {
|
||||
return this.config.get<string>("dmoney.timeoutExpress") ?? "120m";
|
||||
}
|
||||
private get language(): string {
|
||||
return this.config.get<string>("dmoney.language") ?? "en";
|
||||
}
|
||||
private get currency(): string {
|
||||
return this.config.get<string>("dmoney.currency") ?? "FDJ";
|
||||
}
|
||||
private get privateKey(): string {
|
||||
return this.config.get<string>("dmoney.privateKey") ?? "";
|
||||
}
|
||||
private get publicKey(): string {
|
||||
return this.config.get<string>("dmoney.publicKey") ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export interface DMoneyFabricTokenResponse {
|
||||
/** Returned already prefixed with "Bearer " — set Authorization to this value verbatim. */
|
||||
token: string;
|
||||
effectiveDate?: string;
|
||||
expirationDate?: string;
|
||||
}
|
||||
|
||||
export interface DMoneyPreOrderBizContent {
|
||||
notify_url: string;
|
||||
appid: string;
|
||||
merch_code: string;
|
||||
merch_order_id: string;
|
||||
trade_type: 'Checkout';
|
||||
title: string;
|
||||
total_amount: string;
|
||||
trans_currency: string;
|
||||
timeout_express: string;
|
||||
business_type?: string;
|
||||
redirect_url?: string;
|
||||
callback_info?: string;
|
||||
}
|
||||
|
||||
export interface DMoneyPreOrderRequest {
|
||||
timestamp: string;
|
||||
nonce_str: string;
|
||||
method: 'payment.preorder';
|
||||
version: '1.0';
|
||||
biz_content: DMoneyPreOrderBizContent;
|
||||
sign: string;
|
||||
sign_type: 'SHA256WithRSA';
|
||||
}
|
||||
|
||||
export interface DMoneyPreOrderResponse {
|
||||
result?: 'SUCCESS' | 'FAIL';
|
||||
code?: string;
|
||||
msg?: string;
|
||||
nonce_str?: string;
|
||||
sign?: string;
|
||||
sign_type?: string;
|
||||
biz_content?: {
|
||||
merch_order_id?: string;
|
||||
prepay_id?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type DMoneyOrderStatus =
|
||||
| 'PAY_SUCCESS'
|
||||
| 'PAY_FAILED'
|
||||
| 'WAIT_PAY'
|
||||
| 'ORDER_CLOSED'
|
||||
| 'PAYING'
|
||||
| 'Completed'
|
||||
| 'Failure'
|
||||
| 'Expired'
|
||||
| 'Paying';
|
||||
|
||||
export interface DMoneyQueryOrderResponse {
|
||||
result?: 'SUCCESS' | 'FAIL';
|
||||
code?: string;
|
||||
msg?: string;
|
||||
nonce_str?: string;
|
||||
sign?: string;
|
||||
sign_type?: string;
|
||||
biz_content?: {
|
||||
merch_order_id?: string;
|
||||
order_status?: DMoneyOrderStatus | string;
|
||||
payment_order_id?: string;
|
||||
trans_time?: string;
|
||||
trans_currency?: string;
|
||||
total_amount?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
export interface DMoneyWebhookPayload {
|
||||
merchantId: string;
|
||||
merchantOrderId: string;
|
||||
orderId: string;
|
||||
status: string;
|
||||
transactionId?: string;
|
||||
amount?: string;
|
||||
currency?: string;
|
||||
paidAt?: string;
|
||||
payerPhone?: string;
|
||||
signature: string;
|
||||
appid: string;
|
||||
merch_code: string;
|
||||
merch_order_id: string;
|
||||
payment_order_id: string;
|
||||
notify_time?: string;
|
||||
trans_end_time?: string;
|
||||
total_amount?: string;
|
||||
trans_currency?: string;
|
||||
/** Paying | Expired | Completed | Failure */
|
||||
trade_status: string;
|
||||
transId?: string;
|
||||
callback_info?: string;
|
||||
notify_url?: string;
|
||||
sign: string;
|
||||
sign_type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user