264 lines
8.1 KiB
TypeScript
264 lines
8.1 KiB
TypeScript
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] || 'نتیجه نامشخص تراکنش';
|
||
}
|
||
}
|