This commit is contained in:
ghost2023
2026-06-09 16:37:11 +03:00
parent eda21e22d8
commit 4afe07c19b

View File

@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from '@nestjs/config'; import { ConfigService } from "@nestjs/config";
import { HttpService } from '@nestjs/axios'; import { HttpService } from "@nestjs/axios";
import { import {
PaymentProvider, PaymentProvider,
ProviderInitiationInput, ProviderInitiationInput,
@@ -8,10 +8,10 @@ import {
ProviderStatus, ProviderStatus,
ProviderPaymentStatus, ProviderPaymentStatus,
ProviderMethod, ProviderMethod,
} from '@edr/types'; } from "@edr/types";
import { AxiosError, AxiosRequestConfig } from 'axios'; import { AxiosError, AxiosRequestConfig } from "axios";
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from "rxjs";
import * as crypto from 'node:crypto'; import * as crypto from "node:crypto";
interface DMoneyAuthResponse { interface DMoneyAuthResponse {
token: string; token: string;
@@ -56,9 +56,11 @@ export class DMoneyProvider implements PaymentProvider {
constructor( constructor(
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly http: HttpService, private readonly http: HttpService,
) {} ) { }
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> { async initiate(
input: ProviderInitiationInput,
): Promise<ProviderInitiationResult> {
const token = await this.getFabricToken(); const token = await this.getFabricToken();
const amount = (input.amountMinor / 100).toFixed(2); const amount = (input.amountMinor / 100).toFixed(2);
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
@@ -95,8 +97,11 @@ export class DMoneyProvider implements PaymentProvider {
return { return {
providerOrderId: response.orderId, providerOrderId: response.orderId,
clientAction: response.checkoutUrl clientAction: response.checkoutUrl
? { type: 'REDIRECT', url: response.checkoutUrl } ? { type: "REDIRECT", url: response.checkoutUrl }
: { type: 'REDIRECT', url: `${this.baseUrl}/checkout/${response.orderId}` }, : {
type: "REDIRECT",
url: `${this.baseUrl}/checkout/${response.orderId}`,
},
expiresAt, expiresAt,
rawInitiation: { rawInitiation: {
request: this.sanitize(requestBody), request: this.sanitize(requestBody),
@@ -106,7 +111,7 @@ export class DMoneyProvider implements PaymentProvider {
} }
async queryStatus(merchantOrderId: string): Promise<ProviderStatus> { async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
const token = await this.authenticate(); const token = await this.getFabricToken();
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const signature = this.signRequest({ const signature = this.signRequest({
merchantId: this.merchantId, merchantId: this.merchantId,
@@ -130,14 +135,15 @@ export class DMoneyProvider implements PaymentProvider {
return { return {
status: mapped, status: mapped,
providerTxnId: response.transactionId, providerTxnId: response.transactionId,
failureCode: mapped === ProviderPaymentStatus.FAILED ? response.status : undefined, failureCode:
mapped === ProviderPaymentStatus.FAILED ? response.status : undefined,
rawResponse: response as unknown as Record<string, unknown>, rawResponse: response as unknown as Record<string, unknown>,
}; };
} }
verifyWebhookSignature(payload: Record<string, unknown>): boolean { verifyWebhookSignature(payload: Record<string, unknown>): boolean {
const { signature, ...data } = payload; const { signature, ...data } = payload;
if (!signature || typeof signature !== 'string') return false; if (!signature || typeof signature !== "string") return false;
const expectedSignature = this.signRequest(data); const expectedSignature = this.signRequest(data);
return crypto.timingSafeEqual( return crypto.timingSafeEqual(
@@ -152,17 +158,17 @@ export class DMoneyProvider implements PaymentProvider {
private mapStatus(status: string): ProviderPaymentStatus { private mapStatus(status: string): ProviderPaymentStatus {
switch (status?.toUpperCase()) { switch (status?.toUpperCase()) {
case 'SUCCESS': case "SUCCESS":
case 'COMPLETED': case "COMPLETED":
return ProviderPaymentStatus.SUCCEEDED; return ProviderPaymentStatus.SUCCEEDED;
case 'FAILED': case "FAILED":
case 'REJECTED': case "REJECTED":
case 'EXPIRED': case "EXPIRED":
case 'CANCELLED': case "CANCELLED":
return ProviderPaymentStatus.FAILED; return ProviderPaymentStatus.FAILED;
case 'PENDING': case "PENDING":
return ProviderPaymentStatus.REQUIRES_ACTION; return ProviderPaymentStatus.REQUIRES_ACTION;
case 'PROCESSING': case "PROCESSING":
return ProviderPaymentStatus.PROCESSING; return ProviderPaymentStatus.PROCESSING;
default: default:
return ProviderPaymentStatus.PROCESSING; return ProviderPaymentStatus.PROCESSING;
@@ -178,7 +184,9 @@ export class DMoneyProvider implements PaymentProvider {
); );
if (!response.token) { if (!response.token) {
throw new Error(`DMoney authentication failed: ${JSON.stringify(response)}`); throw new Error(
`DMoney authentication failed: ${JSON.stringify(response)}`,
);
} }
return response.token; return response.token;
@@ -186,22 +194,24 @@ export class DMoneyProvider implements PaymentProvider {
private signRequest(data: Record<string, unknown>): string { private signRequest(data: Record<string, unknown>): string {
const sortedKeys = Object.keys(data).sort(); const sortedKeys = Object.keys(data).sort();
const signString = sortedKeys const signString = sortedKeys.map((key) => `${key}=${data[key]}`).join("&");
.map((key) => `${key}=${data[key]}`)
.join('&');
return crypto return crypto
.createHmac('sha256', this.secretKey) .createHmac("sha256", this.secretKey)
.update(signString) .update(signString)
.digest('hex'); .digest("hex");
} }
private async postJson<T>(url: string, body: unknown, token?: string): Promise<T> { private async postJson<T>(
url: string,
body: unknown,
token?: string,
): Promise<T> {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', "Content-Type": "application/json",
}; };
if (token) { if (token) {
headers['Authorization'] = `Bearer ${token}`; headers["Authorization"] = `Bearer ${token}`;
} }
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
@@ -212,7 +222,9 @@ export class DMoneyProvider implements PaymentProvider {
const started = Date.now(); const started = Date.now();
try { try {
const res = await firstValueFrom(this.http.post<T>(url, body, config)); 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`); this.logger.debug(
`DMoney POST ${url} status=${res.status} latency=${Date.now() - started}ms`,
);
return res.data; return res.data;
} catch (err) { } catch (err) {
if (err instanceof AxiosError) { if (err instanceof AxiosError) {
@@ -220,7 +232,9 @@ export class DMoneyProvider implements PaymentProvider {
`DMoney POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)}`, `DMoney POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)}`,
); );
} else { } else {
this.logger.error(`DMoney POST ${url} threw: ${err instanceof Error ? err.message : err}`); this.logger.error(
`DMoney POST ${url} threw: ${err instanceof Error ? err.message : err}`,
);
} }
throw err; throw err;
} }
@@ -232,21 +246,21 @@ export class DMoneyProvider implements PaymentProvider {
} }
private get baseUrl(): string { private get baseUrl(): string {
return this.config.get<string>('dmoney.baseUrl') ?? ''; return this.config.get<string>("dmoney.baseUrl") ?? "";
} }
private get merchantId(): string { private get merchantId(): string {
return this.config.get<string>('dmoney.merchantId') ?? ''; return this.config.get<string>("dmoney.merchantId") ?? "";
} }
private get appSecret(): string { private get appSecret(): string {
return this.config.get<string>('dmoney.appSecret') ?? ''; return this.config.get<string>("dmoney.appSecret") ?? "";
} }
private get secretKey(): string { private get secretKey(): string {
return this.config.get<string>('dmoney.secretKey') ?? ''; return this.config.get<string>("dmoney.secretKey") ?? "";
} }
private get notifyUrl(): string { private get notifyUrl(): string {
return this.config.get<string>('dmoney.notifyUrl') ?? ''; return this.config.get<string>("dmoney.notifyUrl") ?? "";
} }
private get returnUrl(): string { private get returnUrl(): string {
return this.config.get<string>('dmoney.returnUrl') ?? ''; return this.config.get<string>("dmoney.returnUrl") ?? "";
} }
} }