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 { 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 { 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 { 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 { 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 { 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 = { [-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 = { 100: 'با موفقیت تایید شد.', 102: 'مرچنت یافت نشد.', 103: 'مرچنت غیرفعال است یا قرارداد درگاه امضا نشده است.', 104: 'مرچنت نامعتبر است.', 105: 'مبلغ باید بزرگتر از ۱,۰۰۰ ریال باشد.', 106: 'آدرس بازگشت (callbackUrl) نامعتبر است.', 113: 'مبلغ تراکنش از سقف مجاز بیشتر است.', 114: 'کد ملی ارسالی نامعتبر است.', 115: 'آی‌پی سرور شما در پنل کاربری زیبال ثبت نشده است.', 201: 'تراکنش قبلا تایید شده است.', 202: 'سفارش پرداخت نشده یا ناموفق بوده است.', 203: 'شناسه پیگیری (trackId) نامعتبر است.', }; return messages[result ?? 0] || 'نتیجه نامشخص تراکنش'; } }