canina/backend/src/payment/zibal.service.ts

264 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
export interface ZibalRequestResponse {
trackId: number;
result: number;
message: string;
}
export interface ZibalVerifyResponse {
paidAt?: string;
amount?: number;
result: number;
message: string;
status?: number;
refNumber?: string;
description?: string;
cardNumber?: string;
orderId?: string;
}
export interface ZibalInquiryResponse {
paidAt?: string;
amount?: number;
result: number;
message: string;
status?: number;
refNumber?: string;
description?: string;
cardNumber?: string;
orderId?: string;
}
@Injectable()
export class ZibalService {
private readonly logger = new Logger(ZibalService.name);
private readonly baseUrl = 'https://gateway.zibal.ir';
constructor(private readonly prisma: PrismaService) {}
/**
* Retrieves active merchant code from settings (DB) or fallback to env / 'zibal' sandbox.
*/
async getMerchant(): Promise<string> {
try {
const setting = await this.prisma.uiText.findUnique({
where: { key: 'ZIBAL_MERCHANT' },
});
if (setting && setting.value && setting.value.trim() !== '') {
return setting.value.trim();
}
} catch (e) {
this.logger.warn(`Could not read ZIBAL_MERCHANT from db: ${e}`);
}
return process.env.ZIBAL_MERCHANT || 'zibal';
}
/**
* Retrieves base frontend/app url for callbacks
*/
async getFrontendUrl(): Promise<string> {
try {
const setting = await this.prisma.uiText.findUnique({
where: { key: 'FRONTEND_URL' },
});
if (setting && setting.value && setting.value.trim() !== '') {
return setting.value.trim().replace(/\/+$/, '');
}
} catch (e) {
this.logger.warn(`Could not read FRONTEND_URL from db: ${e}`);
}
const envUrl = process.env.FRONTEND_URL || process.env.APP_URL;
return envUrl ? envUrl.replace(/\/+$/, '') : 'https://canina.ir';
}
/**
* 1. Request Payment (درخواست پرداخت)
* Amount must be in Rials.
*/
async requestPayment(params: {
amountRials: number;
callbackUrl: string;
description?: string;
orderId?: string;
mobile?: string;
nationalCode?: string;
allowedCards?: string[];
}): Promise<ZibalRequestResponse> {
const merchant = await this.getMerchant();
const payload = {
merchant,
amount: params.amountRials,
callbackUrl: params.callbackUrl,
description: params.description || 'پرداخت سفارش در پت شاپ کانینا',
orderId: params.orderId,
mobile: params.mobile,
nationalCode: params.nationalCode,
allowedCards: params.allowedCards,
};
this.logger.log(
`Sending payment request to Zibal for orderId=${params.orderId}, amount=${params.amountRials} Rials, merchant=${merchant}`,
);
const response = await fetch(`${this.baseUrl}/v1/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
this.logger.error(
`Zibal request HTTP error ${response.status}: ${errorText}`,
);
throw new Error(
`خطا در اتصال به درگاه زیبال: کد وضعیت ${response.status}`,
);
}
const data = (await response.json()) as ZibalRequestResponse;
this.logger.log(`Zibal request response: ${JSON.stringify(data)}`);
return data;
}
/**
* 2. Generate Start Payment URL (آدرس هدایت به درگاه)
*/
getStartUrl(trackId: string | number): string {
return `${this.baseUrl}/start/${trackId}`;
}
/**
* 3. Verify Payment (تایید تراکنش)
*/
async verifyPayment(trackId: string | number): Promise<ZibalVerifyResponse> {
const merchant = await this.getMerchant();
const payload = {
merchant,
trackId: String(trackId),
};
this.logger.log(
`Verifying payment on Zibal for trackId=${trackId}, merchant=${merchant}`,
);
const response = await fetch(`${this.baseUrl}/v1/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
this.logger.error(
`Zibal verify HTTP error ${response.status}: ${errorText}`,
);
throw new Error(
`خطا در تایید تراکنش درگاه زیبال: کد وضعیت ${response.status}`,
);
}
const data = (await response.json()) as ZibalVerifyResponse;
this.logger.log(`Zibal verify response: ${JSON.stringify(data)}`);
return data;
}
/**
* 4. Inquiry Payment (استعلام وضعیت تراکنش)
*/
async inquiryPayment(
trackId: string | number,
): Promise<ZibalInquiryResponse> {
const merchant = await this.getMerchant();
const payload = {
merchant,
trackId: String(trackId),
};
this.logger.log(`Inquiring payment on Zibal for trackId=${trackId}`);
const response = await fetch(`${this.baseUrl}/v1/inquiry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
this.logger.error(
`Zibal inquiry HTTP error ${response.status}: ${errorText}`,
);
throw new Error(
`خطا در استعلام تراکنش زیبال: کد وضعیت ${response.status}`,
);
}
const data = (await response.json()) as ZibalInquiryResponse;
return data;
}
/**
* Translates status codes from Zibal to Persian human-friendly messages
*/
getStatusMessage(status?: number | string): string {
const statusNum = Number(status);
const messages: Record<number, string> = {
[-1]: 'در انتظار پرداخت',
[-2]: 'خطای داخلی درگاه زیبال',
1: 'پرداخت شده و تایید شده',
2: 'پرداخت شده - در انتظار تایید',
3: 'تراکنش توسط کاربر لغو شد',
4: 'شماره کارت نامعتبر است',
5: 'موجودی حساب کافی نیست',
6: 'رمز وارد شده اشتباه است',
7: 'تعداد درخواست‌ها بیش از حد مجاز است',
8: 'تعداد پرداخت اینترنتی روزانه بیش از حد مجاز است',
9: 'مبلغ پرداخت اینترنتی روزانه بیش از حد مجاز است',
10: 'صادرکننده کارت نامعتبر است',
11: 'خطای سوئیچ بانکی',
12: 'کارت قابل دسترسی نیست',
15: 'تراکنش استرداد شده است',
16: 'تراکنش در حال استرداد است',
18: 'تراکنش ریورس شده است',
21: 'پذیرنده نامعتبر است',
};
return messages[statusNum] || 'وضعیت نامشخص';
}
/**
* Translates verify result codes to Persian messages
*/
getResultMessage(result?: number): string {
const messages: Record<number, string> = {
100: 'با موفقیت تایید شد.',
102: 'مرچنت یافت نشد.',
103: 'مرچنت غیرفعال است یا قرارداد درگاه امضا نشده است.',
104: 'مرچنت نامعتبر است.',
105: 'مبلغ باید بزرگتر از ۱,۰۰۰ ریال باشد.',
106: 'آدرس بازگشت (callbackUrl) نامعتبر است.',
113: 'مبلغ تراکنش از سقف مجاز بیشتر است.',
114: 'کد ملی ارسالی نامعتبر است.',
115: 'آی‌پی سرور شما در پنل کاربری زیبال ثبت نشده است.',
201: 'تراکنش قبلا تایید شده است.',
202: 'سفارش پرداخت نشده یا ناموفق بوده است.',
203: 'شناسه پیگیری (trackId) نامعتبر است.',
};
return messages[result ?? 0] || 'نتیجه نامشخص تراکنش';
}
}