diff --git a/src/driver.spec.ts b/src/driver.spec.ts index 0eb0067..6502534 100644 --- a/src/driver.spec.ts +++ b/src/driver.spec.ts @@ -1,11 +1,12 @@ +import { BadConfigError } from './exceptions'; import { z } from 'zod'; import { defineDriver } from './driver'; const createTestDriver = defineDriver({ schema: { - config: z.object({}), - request: z.object({}), - verify: z.object({}), + config: z.object({ test: z.string() }), + request: z.object({ test: z.string() }), + verify: z.object({ test: z.string() }), }, defaultConfig: {}, request: async () => { @@ -27,8 +28,9 @@ const createTestDriver = defineDriver({ describe('Driver', () => { it('creates javascript raw script for form submission on request()', async () => { - const driver = createTestDriver({}); + const driver = createTestDriver({ test: 'test' }); const paymentInfo = await driver.request({ + test: 'test', amount: 1000, callbackUrl: 'https://callback.url/', description: 'testin', @@ -37,4 +39,32 @@ describe('Driver', () => { expect(typeof paymentInfo.getScript()).toBe('string'); expect(paymentInfo.getScript()).toContain('.submit()'); }); + it('Throws badConfigError when wrong config is passed', () => { + // @ts-ignore + expect(() => createTestDriver({})).toThrow(BadConfigError); + }); + it('Throws badConfigError when wrong request params are passed', async () => { + const driver = createTestDriver({ test: 'test' }); + await expect( + async () => + // @ts-ignore + await driver.request({ + amount: 1000, + callbackUrl: 'https://callback.url/', + description: 'testin', + }), + ).rejects.toThrow(BadConfigError); + }); + it('Throws badConfigError when wrong verify params are passed', async () => { + const driver = createTestDriver({ test: 'test' }); + await expect( + async () => + // @ts-ignore + await driver.verify({ + amount: 1000, + callbackUrl: 'https://callback.url/', + description: 'testin', + }), + ).rejects.toThrow(BadConfigError); + }); }); diff --git a/src/driver.ts b/src/driver.ts index 40f1ea6..55c9459 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -1,5 +1,6 @@ import { z, ZodSchema } from 'zod'; import { buildRedirectScript } from './utils/buildRedirectScript'; +import { safeParse } from './utils/safeParse'; interface IPaymentInfo { referenceId: string | number; @@ -52,10 +53,9 @@ export const defineDriver = < verify: (arg: { ctx: IConfig; options: IVerify; params: Record }) => Promise; }) => { return (config: Omit & Partial) => { - const ctx: IConfig = schema.config.parse({ ...defaultConfig, ...config }); - + const ctx = safeParse(schema.config, { ...defaultConfig, ...config }) as IConfig; const requestPayment = async (options: Parameters['0']['options']) => { - options = schema.request.parse(options); + options = safeParse(schema.request, options) as IRequest; const paymentInfo = await request({ ctx, options }); return { ...paymentInfo, @@ -67,7 +67,7 @@ export const defineDriver = < options: Parameters['0']['options'], params: Parameters['0']['params'], ) => { - options = schema.verify.parse(options); + options = safeParse(schema.verify, options) as IVerify; return verify({ ctx, options, params }); }; diff --git a/src/drivers/behpardakht/api.ts b/src/drivers/behpardakht/api.ts index 7424abb..5535b0e 100644 --- a/src/drivers/behpardakht/api.ts +++ b/src/drivers/behpardakht/api.ts @@ -190,3 +190,33 @@ export const errors: Record = { '55': 'است نامعتبر تراكنش', '61': 'واريز در خطا', }; + +export const IPGConfigErrors = [ + '21', + '24', + '25', + '31', + '32', + '33', + '35', + '41', + '42', + '43', + '44', + '45', + '46', + '47', + '48', + '49', + '412', + '413', + '414', + '417', + '51', + '54', + '55', +]; + +export const IPGUserErrors = ['11', '12', '13', '14', '15', '16', '17', '18', '19', '111', '112', '113', '114']; + +export const IPGFailureErrors = ['23', '34', '416', '61']; diff --git a/src/drivers/behpardakht/behpardakht.spec.ts b/src/drivers/behpardakht/behpardakht.spec.ts index 45da4d8..aa1cfb3 100644 --- a/src/drivers/behpardakht/behpardakht.spec.ts +++ b/src/drivers/behpardakht/behpardakht.spec.ts @@ -1,5 +1,5 @@ import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { BehpardakhtDriver, createBehpardakhtDriver } from './behpardakht'; @@ -34,18 +34,41 @@ describe('Behpardakht Driver', () => { ).toBe('string'); }); - it('throws payment errors accordingly', async () => { - const serverResponse: API.RequestPaymentRes = '100'; + it('throws payment failure accordingly', async () => { + const serverResponse: API.RequestPaymentRes = '34'; + mockSoapClient.bpPayRequest = () => serverResponse; + + await expect( + async () => + await driver.request({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + }), + ).rejects.toThrow(GatewayFailureError); + }); + it('throws bad config error for payment accordingly', async () => { + const serverResponse: API.RequestPaymentRes = '24'; mockSoapClient.bpPayRequest = () => serverResponse; + await expect( + async () => + await driver.request({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + }), + ).rejects.toThrow(BadConfigError); + }); + it('throws user error for payment accordingly', async () => { + const serverResponse: API.RequestPaymentRes = '19'; + mockSoapClient.bpPayRequest = () => serverResponse; await expect( async () => await driver.request({ amount: 20000, callbackUrl: 'https://mysite.com/callback', }), - ).rejects.toThrow(RequestException); + ).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/behpardakht/behpardakht.ts b/src/drivers/behpardakht/behpardakht.ts index b954dbe..2a9dc6c 100644 --- a/src/drivers/behpardakht/behpardakht.ts +++ b/src/drivers/behpardakht/behpardakht.ts @@ -1,7 +1,7 @@ import * as soap from 'soap'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import { generateId } from '../../utils/generateId'; import * as API from './api'; @@ -25,6 +25,13 @@ const timeFormat = (date = new Date()) => { return hh.toString() + mm.toString() + ss.toString(); }; +const throwError = (errorCode: string) => { + const message = API.errors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createBehpardakhtDriver = defineDriver({ schema: { config: z.object({ @@ -73,7 +80,7 @@ export const createBehpardakhtDriver = defineDriver({ const RefId = splittedResponse[1]; if (ResCode.toString() !== '0') { - throw new RequestException(API.errors[response[0]]); + throwError(ResCode); } return { @@ -90,7 +97,7 @@ export const createBehpardakhtDriver = defineDriver({ const { terminalId, username, password, links } = ctx; if (ResCode !== '0') { - throw new PaymentException(API.errors[ResCode]); + throwError(ResCode); } const soapClient = await soap.createClientAsync(links.verify); @@ -111,7 +118,7 @@ export const createBehpardakhtDriver = defineDriver({ if (verifyResponse.toString() !== '43') { soapClient.bpReversalRequest(requestFields); } - throw new VerificationException(API.errors[verifyResponse]); + throwError(ResCode); } // 2. Settle @@ -120,7 +127,7 @@ export const createBehpardakhtDriver = defineDriver({ if (settleResponse.toString() !== '45' && settleResponse.toString() !== '48') { soapClient.bpReversalRequest(requestFields); } - throw new VerificationException(API.errors[verifyResponse]); + throwError(ResCode); } return { diff --git a/src/drivers/idpay/api.ts b/src/drivers/idpay/api.ts index 13b0703..c1200d7 100644 --- a/src/drivers/idpay/api.ts +++ b/src/drivers/idpay/api.ts @@ -48,7 +48,7 @@ export interface RequestPaymentReq { callback: string; } -export interface RequestPaymenRes_Successful { +export interface RequestPaymentRes_Successful { /** * کلید منحصر بفرد تراکنش */ @@ -65,7 +65,7 @@ export interface RequestPaymentRes_Failed { error_message: string; } -export type RequestPaymentRes = RequestPaymenRes_Successful | RequestPaymentRes_Failed; +export type RequestPaymentRes = RequestPaymentRes_Successful | RequestPaymentRes_Failed; export interface CallbackParams_POST { /** @@ -271,3 +271,28 @@ export const errors: Record = { // 405 '54': 'مدت زمان تایید پرداخت سپری شده است.', }; + +export const IPGConfigErrors = [ + '11', + '12', + '13', + '14', + '21', + '22', + '23', + '24', + '31', + '32', + '33', + '34', + '35', + '36', + '37', + '38', + '41', + '42', + '43', + '54', +]; + +export const IPGUserErrors = ['1', '2', '7']; diff --git a/src/drivers/idpay/idpay.spec.ts b/src/drivers/idpay/idpay.spec.ts index 2a12d5a..eb1c2a7 100644 --- a/src/drivers/idpay/idpay.spec.ts +++ b/src/drivers/idpay/idpay.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createIdpayDriver, IdpayDriver } from './idpay'; @@ -35,7 +35,29 @@ describe('IdPay Driver', () => { mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config error accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + error_code: 23, + error_message: 'Some error happened', + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(BadConfigError); + }); + + it('throws payment user error accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + error_code: 7, + error_message: 'Some error happened', + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/idpay/idpay.ts b/src/drivers/idpay/idpay.ts index 422d013..9dfa68a 100644 --- a/src/drivers/idpay/idpay.ts +++ b/src/drivers/idpay/idpay.ts @@ -1,15 +1,23 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import { generateUuid } from '../../utils/generateUuid'; import * as API from './api'; +import { RequestPaymentRes_Successful, VerifyPaymentRes_Successful } from './api'; const getHeaders = (apiKey: string, sandbox: boolean) => ({ 'X-SANDBOX': sandbox ? '1' : '0', 'X-API-KEY': apiKey, }); +const throwError = (errorCode: string) => { + const message = API.errors[errorCode] ?? API.callbackErrors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createIdpayDriver = defineDriver({ schema: { config: z.object({ @@ -55,8 +63,9 @@ export const createIdpayDriver = defineDriver({ if ('error_message' in response.data) { const error = response.data as API.RequestPaymentRes_Failed; - throw new RequestException(API.errors[error.error_code.toString()]); + throwError(error.error_code.toString()); } + response.data = response.data as RequestPaymentRes_Successful; return { method: 'GET', referenceId: response.data.id, @@ -66,9 +75,9 @@ export const createIdpayDriver = defineDriver({ verify: async ({ ctx, params }) => { const { apiKey, links, sandbox } = ctx; const { id, order_id, status } = params; - - if (status.toString() !== '200') { - throw new PaymentException(API.callbackErrors[status.toString()]); + const statusCode = status.toString(); + if (statusCode !== '200') { + throwError(statusCode); } const response = await axios.post( @@ -83,9 +92,9 @@ export const createIdpayDriver = defineDriver({ ); if ('error_message' in response.data) { - throw new VerificationException(API.callbackErrors[response.data.error_code.toString()]); + throwError(response.data.error_code.toString()); } - + response.data = response.data as VerifyPaymentRes_Successful; return { transactionId: response.data.track_id, cardPan: response.data.payment.card_no, diff --git a/src/drivers/nextpay/api.ts b/src/drivers/nextpay/api.ts index 0bd2f8e..42efcda 100644 --- a/src/drivers/nextpay/api.ts +++ b/src/drivers/nextpay/api.ts @@ -238,3 +238,53 @@ export const errors: Record = { '-93': 'موجودی صندوق کاربری برای بازگشت مبلغ کافی نیست', '-94': 'کلید بازگشت مبلغ یافت نشد', }; + +export const IPGConfigErrors = [ + '-20', + '-21', + '-22', + '-23', + '-24', + '-25', + '-26', + '-27', + '-28', + '-29', + '-30', + '-31', + '-32', + '-33', + '-34', + '-35', + '-36', + '-37', + '-38', + '-39', + '-40', + '-41', + '-44', + '-46', + '-47', + '-48', + '-49', + '-50', + '-51', + '-52', + '-60', + '-61', + '-62', + '-63', + '-64', + '-65', + '-66', + '-67', + '-68', + '-69', + '-70', + '-71', + '-73', + '-93', + '-94', +]; + +export const IPGUserErrors = ['-2', '-4']; diff --git a/src/drivers/nextpay/nextpay.spec.ts b/src/drivers/nextpay/nextpay.spec.ts index 5695c0b..cb64ecb 100644 --- a/src/drivers/nextpay/nextpay.spec.ts +++ b/src/drivers/nextpay/nextpay.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createNextpayDriver, NextpayDriver } from './nextpay'; @@ -35,7 +35,29 @@ describe('NextPay Driver', () => { mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + trans_id: '1234', + code: -61, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(BadConfigError); + }); + + it('throws payment user errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + trans_id: '1234', + code: -4, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/nextpay/nextpay.ts b/src/drivers/nextpay/nextpay.ts index 5a37100..87163dc 100644 --- a/src/drivers/nextpay/nextpay.ts +++ b/src/drivers/nextpay/nextpay.ts @@ -3,9 +3,16 @@ import { z } from 'zod'; import { generateUuid } from '../../utils/generateUuid'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; +const throwError = (errorCode: string) => { + const message = API.errors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createNextpayDriver = defineDriver({ schema: { config: z.object({ @@ -44,8 +51,10 @@ export const createNextpayDriver = defineDriver({ const { code, trans_id } = response.data; - if (code.toString() !== '0') { - throw new RequestException(API.errors[code.toString()]); + const responseCode = code.toString(); + + if (responseCode !== '0') { + throwError(responseCode); } return { @@ -59,7 +68,7 @@ export const createNextpayDriver = defineDriver({ const { apiKey, links } = ctx; if (!trans_id) { - throw new PaymentException('تراکنش توسط کاربر لغو شد.'); + throw new UserError({ message: 'تراکنش توسط کاربر لغو شد.' }); } const response = await axios.post(links.verify, { @@ -70,8 +79,10 @@ export const createNextpayDriver = defineDriver({ const { Shaparak_Ref_Id, code, card_holder } = response.data; - if (code.toString() !== '0') { - throw new VerificationException(API.errors[code.toString()]); + const responseCode = code.toString(); + + if (responseCode !== '0') { + throwError(responseCode); } return { diff --git a/src/drivers/parsian/parsian.spec.ts b/src/drivers/parsian/parsian.spec.ts index e433f55..f954dbb 100644 --- a/src/drivers/parsian/parsian.spec.ts +++ b/src/drivers/parsian/parsian.spec.ts @@ -1,5 +1,5 @@ import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { GatewayFailureError } from '../../exceptions'; import * as API from './api'; import { createParsianDriver, ParsianDriver } from './parsian'; @@ -46,7 +46,7 @@ describe('Parsian Driver', () => { amount: 20000, callbackUrl: 'https://mysite.com/callback', }), - ).rejects.toThrow(RequestException); + ).rejects.toThrow(GatewayFailureError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/parsian/parsian.ts b/src/drivers/parsian/parsian.ts index cd98e3a..d589703 100644 --- a/src/drivers/parsian/parsian.ts +++ b/src/drivers/parsian/parsian.ts @@ -1,7 +1,7 @@ import * as soap from 'soap'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { GatewayFailureError, UserError } from '../../exceptions'; import { generateId } from '../../utils/generateId'; import * as API from './api'; @@ -42,7 +42,7 @@ export const createParsianDriver = defineDriver({ const { Status, Token } = response; if (Status.toString() !== '0' || typeof Token === 'undefined') { - throw new RequestException('خطایی در درخواست پرداخت به‌وجود آمد'); + throw new GatewayFailureError({ message: 'خطایی در درخواست پرداخت به‌وجود آمد' }); } return { @@ -57,7 +57,7 @@ export const createParsianDriver = defineDriver({ const { merchantId, links } = ctx; if (status.toString() !== '0') { - throw new PaymentException('تراکنش توسط کاربر لغو شد.'); + throw new UserError({ message: 'تراکنش توسط کاربر لغو شد.' }); } const soapClient = await soap.createClientAsync(links.verify); @@ -75,9 +75,9 @@ export const createParsianDriver = defineDriver({ const reversalRequestFields: API.ReversalPaymentReq = requestFields; const reversalResponse: API.ReversalPaymentRes = soapClient.ReversalRequest(reversalRequestFields); if (reversalResponse.Status !== '0') { - throw new VerificationException('خطایی در تایید پرداخت به‌وجود آمد و مبلغ بازگشته نشد.'); + throw new GatewayFailureError({ message: 'خطایی در تایید پرداخت به‌وجود آمد و مبلغ بازگشته نشد.' }); } - throw new VerificationException('خطایی در تایید پرداخت به‌وجود آمد'); + throw new GatewayFailureError({ message: 'خطایی در تایید پرداخت به‌وجود آمد' }); } return { diff --git a/src/drivers/pasargad/pasargad.spec.ts b/src/drivers/pasargad/pasargad.spec.ts index 53dcf8c..e8180d2 100644 --- a/src/drivers/pasargad/pasargad.spec.ts +++ b/src/drivers/pasargad/pasargad.spec.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import * as fs from 'fs/promises'; import { Receipt } from '../../driver'; import { getPaymentDriver } from '../../drivers'; -import { RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError } from '../../exceptions'; import * as API from './api'; import { createPasargadDriver, PasargadDriver } from './pasargad'; @@ -22,8 +22,6 @@ const mockKey = ` io7Xyef97NzU5qg0ULDKzBEo+BolEotN0799aNtfRZTzZ08kPGTMF7X0ZSmvcNqfTu4+7wKNRNH/fq47pj0ESNsWVt1FkQu/upp6uTzdiFF2xjcouA8NCLhdV1/VJjtINJq3M8AUT8Qa5VvDTbzL5bxyvWfIqxZVWU0k7XGEVak= `; -mockedFs.readFile.mockResolvedValue(Buffer.from(mockKey)); - describe('Pasargad', () => { let driver: PasargadDriver; @@ -42,6 +40,7 @@ describe('Pasargad', () => { Token: 'PAYMENT_TOKEN', }; mockedAxios.post.mockResolvedValueOnce({ data: getTokenResponse }); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(mockKey)); expect( typeof ( @@ -62,6 +61,7 @@ describe('Pasargad', () => { Message: 'تراکنش ارسالی معتبر نیست', }; mockedAxios.post.mockResolvedValueOnce({ data: getTokenResponse }); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(mockKey)); await expect( async () => @@ -71,7 +71,7 @@ describe('Pasargad', () => { invoiceDate: new Date().toISOString(), invoiceNumber: '12', }), - ).rejects.toThrow(RequestException); + ).rejects.toThrow(GatewayFailureError); }); it('verifies the purchase correctly', async () => { const serverResponse: API.VerifyPaymentRes = { @@ -89,6 +89,7 @@ describe('Pasargad', () => { }; mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(mockKey)); expect( await driver.verify({ amount: 2000 }, { iD: new Date().toISOString(), iN: '123', tref: '123456' }), @@ -101,6 +102,19 @@ describe('Pasargad', () => { Message: 'تراکنش ارسالی معتبر نیست', }; mockedAxios.post.mockResolvedValueOnce({ data: verifyResponse }); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(mockKey)); + + const driver = getPaymentDriver('pasargad')({ + privateKeyXMLFile: './something.xml', + merchantId: '123', + terminalId: '123', + }); + await expect( + driver.verify({ amount: 2000 }, { iD: new Date().toISOString(), iN: '123', tref: '1234' }), + ).rejects.toThrow(GatewayFailureError); + }); + it('throws Bad config error on invalid key', async () => { + mockedFs.readFile.mockResolvedValueOnce(Buffer.from('--wrong rsa-xml--')); const driver = getPaymentDriver('pasargad')({ privateKeyXMLFile: './something.xml', merchantId: '123', @@ -108,6 +122,6 @@ describe('Pasargad', () => { }); await expect( driver.verify({ amount: 2000 }, { iD: new Date().toISOString(), iN: '123', tref: '1234' }), - ).rejects.toThrow(VerificationException); + ).rejects.toThrow(BadConfigError); }); }); diff --git a/src/drivers/pasargad/pasargad.ts b/src/drivers/pasargad/pasargad.ts index 18c8eca..5cfc41e 100644 --- a/src/drivers/pasargad/pasargad.ts +++ b/src/drivers/pasargad/pasargad.ts @@ -4,7 +4,7 @@ import * as crypto from 'crypto'; import * as fs from 'fs/promises'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError } from '../../exceptions'; import * as API from './api'; const getCurrentTimestamp = (): string => { @@ -13,12 +13,16 @@ const getCurrentTimestamp = (): string => { }; const signData = async (data: unknown, privateKeyXMLFile: string): Promise => { - const sign = crypto.createSign('SHA1'); - sign.write(JSON.stringify(data)); - sign.end(); - const pemKey = await convertXmlToPemKey(privateKeyXMLFile); - const signedData = sign.sign(Buffer.from(pemKey), 'base64'); - return signedData; + try { + const sign = crypto.createSign('SHA1'); + sign.write(JSON.stringify(data)); + sign.end(); + const pemKey = await convertXmlToPemKey(privateKeyXMLFile); + const signedData = sign.sign(Buffer.from(pemKey), 'base64'); + return signedData; + } catch (err) { + throw new BadConfigError({ message: 'The signing process has failed. Error: ' + err, isIPGError: false }); + } }; const convertXmlToPemKey = async (xmlFilePath: string): Promise => { @@ -83,7 +87,7 @@ export const createPasargadDriver = defineDriver({ }); if (!response.data?.IsSuccess) { - throw new RequestException(errorMessage); + throw new GatewayFailureError({ message: errorMessage }); } return { method: 'GET', @@ -111,7 +115,7 @@ export const createPasargadDriver = defineDriver({ Sign: await signData(data, privateKeyXMLFile), }, }); - if (!response.data?.IsSuccess) throw new VerificationException(errorMessage); + if (!response.data?.IsSuccess) throw new GatewayFailureError({ message: errorMessage }); return { raw: response.data, transactionId: tref, diff --git a/src/drivers/payir/api.ts b/src/drivers/payir/api.ts index 9e2f54c..1c9493b 100644 --- a/src/drivers/payir/api.ts +++ b/src/drivers/payir/api.ts @@ -117,3 +117,31 @@ export const errors: Record = { '-25': 'امکان استفاده از سرویس در کشور مبدا شما وجود نداره', '-26': 'امکان انجام تراکنش برای این درگاه وجود ندارد', }; + +export const IPGConfigErrors = [ + '-1', + '-2', + '-3', + '-4', + '-6', + '-7', + '-8', + '-9', + '-10', + '-11', + '-12', + '-13', + '-14', + '-16', + '-17', + '-18', + '-19', + '-20', + '-21', + '-22', + '-24', + '-25', + '-26', +]; + +export const IPGUserErrors = ['-5', '-15']; diff --git a/src/drivers/payir/payir.spec.ts b/src/drivers/payir/payir.spec.ts index dae00c3..cc367bc 100644 --- a/src/drivers/payir/payir.spec.ts +++ b/src/drivers/payir/payir.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createPayirDriver, PayirDriver } from './payir'; @@ -38,7 +38,31 @@ describe('Payir Driver', () => { mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + status: -10, + errorMessage: 'some error', + token: '1234', + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(BadConfigError); + }); + + it('throws payment user errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + status: -5, + errorMessage: 'some error', + token: '1234', + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/payir/payir.ts b/src/drivers/payir/payir.ts index d1fe037..1f4f908 100644 --- a/src/drivers/payir/payir.ts +++ b/src/drivers/payir/payir.ts @@ -1,11 +1,18 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; const getApiKey = (apiKey: string, sandbox: boolean) => (sandbox ? 'test' : apiKey); +const throwError = (errorCode: string) => { + const message = API.errors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createPayirDriver = defineDriver({ schema: { config: z.object({ @@ -45,10 +52,10 @@ export const createPayirDriver = defineDriver({ validCardNumber, }); - const { status } = response.data; + const statusCode = response.data.status.toString(); - if (status.toString() !== '1') { - throw new RequestException(API.errors[status.toString()]); + if (statusCode !== '1') { + throwError(statusCode); } response.data = response.data as API.RequestPaymentRes_Success; @@ -63,8 +70,9 @@ export const createPayirDriver = defineDriver({ const { status, token } = params; const { apiKey, sandbox, links } = ctx; - if (status.toString() !== '1') { - throw new PaymentException(API.errors[status.toString()]); + const statusCode = status.toString(); + if (statusCode !== '1') { + throwError(statusCode); } const response = await axios.post(links.verify, { @@ -72,10 +80,10 @@ export const createPayirDriver = defineDriver({ token, }); - const verifyStatus = response.data.status; + const verifyStatus = response.data.status.toString(); - if (verifyStatus.toString() !== '1') { - throw new VerificationException(API.errors[verifyStatus.toString()]); + if (verifyStatus !== '1') { + throwError(verifyStatus); } response.data = response.data as API.VerifyPaymentRes_Success; diff --git a/src/drivers/payping/payping.spec.ts b/src/drivers/payping/payping.spec.ts index e891fb5..d82dd85 100644 --- a/src/drivers/payping/payping.spec.ts +++ b/src/drivers/payping/payping.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { GatewayFailureError } from '../../exceptions'; import * as API from './api'; import { createPaypingDriver, PaypingDriver } from './payping'; @@ -31,7 +31,7 @@ describe('PayPing Driver', () => { // mockedAxios.post.mockRejectedValueOnce({ response: { status: 401 } }); mockedAxios.post.mockReturnValueOnce(Promise.reject({ response: { status: 401 } })); - expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/payping/payping.ts b/src/drivers/payping/payping.ts index 9095256..9e8f7ed 100644 --- a/src/drivers/payping/payping.ts +++ b/src/drivers/payping/payping.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { RequestException, VerificationException } from '../../exceptions'; +import { GatewayFailureError } from '../../exceptions'; import * as API from './api'; const statusToMessage = (status = 500) => { @@ -64,7 +64,7 @@ export const createPaypingDriver = defineDriver({ }, ); } catch (error) { - throw new RequestException(statusToMessage((error as any).response.status)); + throw new GatewayFailureError({ message: statusToMessage((error as any).response.status) }); } const { code } = response.data; @@ -95,7 +95,7 @@ export const createPaypingDriver = defineDriver({ }, ); } catch (error) { - throw new VerificationException(statusToMessage((error as any).response.status)); + throw new GatewayFailureError({ message: statusToMessage((error as any).response.status) }); } const { cardNumber } = response.data; diff --git a/src/drivers/sadad/api.ts b/src/drivers/sadad/api.ts index e2b0739..4fdfeae 100644 --- a/src/drivers/sadad/api.ts +++ b/src/drivers/sadad/api.ts @@ -231,3 +231,39 @@ export const verifyErrors: Record = { '-1': 'پارامترهای ارسالی صحیح نیست و يا تراکنش در سیستم وجود ندارد', '101': 'مهلت ارسال تراکنش به پايان رسیده است', }; + +export const IPGConfigErrors = [ + '3', + '23', + '58', + '61', + '1000', + '1001', + '1003', + '1004', + '1011', + '1012', + '1017', + '1018', + '1019', + '1020', + '1023', + '1024', + '1025', + '1026', + '1027', + '1028', + '1029', + '1030', + '1033', + '1036', + '1037', + '1053', + '1055', + '1101', + '1103', + '1104', + '1105', + '-1', + '101', +]; diff --git a/src/drivers/sadad/sadad.spec.ts b/src/drivers/sadad/sadad.spec.ts index f7659f2..7d396dc 100644 --- a/src/drivers/sadad/sadad.spec.ts +++ b/src/drivers/sadad/sadad.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError } from '../../exceptions'; import * as API from './api'; import { createSadadDriver, SadadDriver } from './sadad'; @@ -41,7 +41,7 @@ describe('Sadad Driver', () => { it('throws payment errors accordingly', async () => { const serverResponse: API.RequestPaymentRes = { Token: 'some-token', - ResCode: 3, + ResCode: 1068, Description: 'description', }; @@ -54,7 +54,26 @@ describe('Sadad Driver', () => { callbackUrl: 'https://callback.url/', mobile: '09120000000', }), - ).rejects.toThrow(RequestException); + ).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + Token: 'some-token', + ResCode: 1026, + Description: 'description', + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect( + async () => + await driver.request({ + amount: 20000, + callbackUrl: 'https://callback.url/', + mobile: '09120000000', + }), + ).rejects.toThrow(BadConfigError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/sadad/sadad.ts b/src/drivers/sadad/sadad.ts index 6b91d18..12348a3 100644 --- a/src/drivers/sadad/sadad.ts +++ b/src/drivers/sadad/sadad.ts @@ -2,18 +2,28 @@ import axios from 'axios'; import * as CryptoJS from 'crypto-js'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError } from '../../exceptions'; import { generateId } from '../../utils/generateId'; import * as API from './api'; const signData = (message: string, key: string): string => { - const keyHex = CryptoJS.enc.Utf8.parse(key); - const encrypted = CryptoJS.DES.encrypt(message, keyHex, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }); + try { + const keyHex = CryptoJS.enc.Utf8.parse(key); + const encrypted = CryptoJS.DES.encrypt(message, keyHex, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + + return encrypted.toString(); + } catch (err) { + throw new BadConfigError({ message: 'The signing process has failed. Error: ' + err, isIPGError: false }); + } +}; - return encrypted.toString(); +const throwError = (errorCode: string) => { + const message = API.requestErrors[errorCode] ?? API.verifyErrors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); }; export const createSadadDriver = defineDriver({ @@ -61,7 +71,7 @@ export const createSadadDriver = defineDriver({ }); if (response.data.ResCode !== 0) { - throw new RequestException(API.requestErrors[response.data.ResCode.toString()]); + throwError(response.data.ResCode.toString()); } return { @@ -78,7 +88,7 @@ export const createSadadDriver = defineDriver({ const { terminalKey, links } = ctx; if (ResCode !== 0) { - throw new PaymentException('تراکنش توسط کاربر لغو شد.'); + throwError(ResCode.toString()); } const response = await axios.post(links.verify, { @@ -89,7 +99,7 @@ export const createSadadDriver = defineDriver({ const { ResCode: verificationResCode, SystemTraceNo } = response.data; if (verificationResCode !== 0) { - throw new VerificationException(API.verifyErrors[verificationResCode.toString()]); + throwError(verificationResCode.toString()); } return { diff --git a/src/drivers/saman/api.ts b/src/drivers/saman/api.ts index 1f6a848..7001cd9 100644 --- a/src/drivers/saman/api.ts +++ b/src/drivers/saman/api.ts @@ -111,3 +111,28 @@ export const callbackErrors: Record = { // export interface VerifyPaymentReq {} export type VerifyPaymentRes = number; + +export const IPGConfigErrors = [ + '-1', + '-3', + '-4', + '-6', + '-7', + '-8', + '-9', + '-10', + '-11', + '-12', + '-13', + '-14', + '-15', + '-17', + '-18', + '5', + '8', + '10', + '11', + '12', +]; + +export const IPGUserErrors = ['1', '4']; diff --git a/src/drivers/saman/saman.spec.ts b/src/drivers/saman/saman.spec.ts index f918e2a..5ce910c 100644 --- a/src/drivers/saman/saman.spec.ts +++ b/src/drivers/saman/saman.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createSamanDriver, SamanDriver } from './saman'; @@ -56,7 +56,43 @@ describe('Saman Driver', () => { callbackUrl: 'https://mysite.com/callback', mobile: '09120000000', }), - ).rejects.toThrow(RequestException); + ).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + errorCode: 5, + status: -1, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect( + async () => + await driver.request({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + mobile: '09120000000', + }), + ).rejects.toThrow(BadConfigError); + }); + + it('throws payment user errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + errorCode: 1, + status: -1, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect( + async () => + await driver.request({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + mobile: '09120000000', + }), + ).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/saman/saman.ts b/src/drivers/saman/saman.ts index 1238dd8..48910b6 100644 --- a/src/drivers/saman/saman.ts +++ b/src/drivers/saman/saman.ts @@ -2,9 +2,16 @@ import axios from 'axios'; import * as soap from 'soap'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; +const throwError = (errorCode: string) => { + const message = API.purchaseErrors[errorCode] ?? API.callbackErrors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createSamanDriver = defineDriver({ schema: { config: z.object({ @@ -41,11 +48,11 @@ export const createSamanDriver = defineDriver({ }); if (response.data.status !== 1 && response.data.errorCode !== undefined) { - throw new RequestException(API.purchaseErrors[response.data.errorCode.toString()]); + throwError(response.data.errorCode.toString()); } if (!response.data.token) { - throw new RequestException(); + throw new GatewayFailureError(); } return { @@ -62,7 +69,7 @@ export const createSamanDriver = defineDriver({ const { RefNum: referenceId, TraceNo: transactionId, Status: status } = params; const { merchantId, links } = ctx; if (!referenceId) { - throw new PaymentException(API.purchaseErrors[status.toString()]); + throwError(status.toString()); } const soapClient = await soap.createClientAsync(links.verify); @@ -70,7 +77,7 @@ export const createSamanDriver = defineDriver({ const responseStatus = +(await soapClient.verifyTransaction(referenceId, merchantId)); if (responseStatus < 0) { - throw new VerificationException(API.purchaseErrors[responseStatus]); + throwError(responseStatus.toString()); } return { diff --git a/src/drivers/vandar/vandar.spec.ts b/src/drivers/vandar/vandar.spec.ts index 2ab464e..795d0f3 100644 --- a/src/drivers/vandar/vandar.spec.ts +++ b/src/drivers/vandar/vandar.spec.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -import { PaymentException, VerificationException } from '../..'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { GatewayFailureError } from '../../exceptions'; import * as API from './api'; import { createVandarDriver, VandarDriver } from './vandar'; @@ -39,7 +38,7 @@ describe('Vandar Driver', () => { mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - expect(driver.request({ amount: 2000, callbackUrl: 'https://example.com' })).rejects.toThrow(RequestException); + expect(driver.request({ amount: 2000, callbackUrl: 'https://example.com' })).rejects.toThrow(GatewayFailureError); }); it('should verify the purchase', async () => { @@ -77,7 +76,7 @@ describe('Vandar Driver', () => { const payment_status = 'NOK'; // Not documented! const amount = 2000; - expect(driver.verify({ amount }, { token, payment_status })).rejects.toThrow(PaymentException); + expect(driver.verify({ amount }, { token, payment_status })).rejects.toThrow(GatewayFailureError); }); it('should throw payment error', async () => { @@ -92,6 +91,6 @@ describe('Vandar Driver', () => { const payment_status = 'OK'; const amount = 2000; - expect(driver.verify({ amount }, { token, payment_status })).rejects.toThrow(VerificationException); + expect(driver.verify({ amount }, { token, payment_status })).rejects.toThrow(GatewayFailureError); }); }); diff --git a/src/drivers/vandar/vandar.ts b/src/drivers/vandar/vandar.ts index f175179..9a01cc5 100644 --- a/src/drivers/vandar/vandar.ts +++ b/src/drivers/vandar/vandar.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { GatewayFailureError } from '../../exceptions'; import * as API from './api'; export const createVandarDriver = defineDriver({ @@ -62,12 +62,9 @@ export const createVandarDriver = defineDriver({ ); const { errors, token } = response.data; - if (errors?.length) { - throw new RequestException(errors.join('\n')); - } + if (errors?.length) throw new GatewayFailureError({ message: errors.join('\n') }); - // TODO: Throw an approperiate error here - if (!token) throw Error('No token provided'); + if (!token) throw new GatewayFailureError({ message: 'No token was provided by the IPG' }); return { method: 'GET', @@ -79,9 +76,7 @@ export const createVandarDriver = defineDriver({ const { token, payment_status } = params; const { api_key, links } = ctx; - if (payment_status !== 'OK') { - throw new PaymentException(); - } + if (payment_status !== 'OK') throw new GatewayFailureError(); const response = await axios.post( links.verify, @@ -96,14 +91,9 @@ export const createVandarDriver = defineDriver({ ); const { errors, transId, cardNumber } = response.data; - if (errors?.length) { - throw new VerificationException(errors.join('\n')); - } + if (errors?.length) throw new GatewayFailureError({ message: errors.join('\n') }); - // TODO: Throw an approperiate error here - if (typeof transId === 'undefined') { - throw Error('No transaction ID provided'); - } + if (transId === undefined) throw new GatewayFailureError({ message: 'No transaction ID was provided by the IPG' }); return { transactionId: transId, diff --git a/src/drivers/zarinpal/api.ts b/src/drivers/zarinpal/api.ts index d4a29ea..5743e08 100644 --- a/src/drivers/zarinpal/api.ts +++ b/src/drivers/zarinpal/api.ts @@ -125,3 +125,7 @@ export const verifyErrors: Record = { '-54': 'اتوریتی نامعتبر است.', '101': 'تراکنش قبلا یک بار تایید شده است.', }; + +export const IPGConfigErrors = ['-9', '-10', '-11', '-12', '-15', '-16', '-50', '-53', '-54', '101']; + +export const IPGUserErrors = ['-51']; diff --git a/src/drivers/zarinpal/zarinpal.spec.ts b/src/drivers/zarinpal/zarinpal.spec.ts index 6033547..5073be4 100644 --- a/src/drivers/zarinpal/zarinpal.spec.ts +++ b/src/drivers/zarinpal/zarinpal.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createZarinpalDriver, ZarinpalDriver } from './zarinpal'; @@ -30,12 +30,34 @@ describe('Zarinpal Driver', () => { it('throws payment errors accordingly', async () => { const serverResponse: API.RequestPaymentRes = { data: [], - errors: { code: -11, message: 'Some error happened from zarinpal', validations: [] }, + errors: { code: -111, message: 'Some error happened from zarinpal', validations: [] }, }; mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + data: [], + errors: { code: -16, message: 'Some error happened from zarinpal', validations: [] }, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(BadConfigError); + }); + + it('throws payment user errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + data: [], + errors: { code: -51, message: 'Some error happened from zarinpal', validations: [] }, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/zarinpal/zarinpal.ts b/src/drivers/zarinpal/zarinpal.ts index 683fb0c..378f4a5 100644 --- a/src/drivers/zarinpal/zarinpal.ts +++ b/src/drivers/zarinpal/zarinpal.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; const getLinks = (links: { request: string; verify: string; payment: string }, sandbox: boolean) => @@ -13,6 +13,13 @@ const getLinks = (links: { request: string; verify: string; payment: string }, s } : links; +const throwError = (errorCode: string) => { + const message = API.requestErrors[errorCode] ?? API.verifyErrors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createZarinpalDriver = defineDriver({ schema: { config: z.object({ @@ -59,10 +66,9 @@ export const createZarinpalDriver = defineDriver({ if (!Array.isArray(errors)) { // There are errors (`errors` is an object) - const { code } = errors; - throw new RequestException(API.requestErrors[code.toString()]); + throwError(errors.code.toString()); } - throw new RequestException(); + throw new GatewayFailureError(); }, verify: async ({ ctx, options, params }) => { const { Authority: authority, Status: status } = params; @@ -70,9 +76,7 @@ export const createZarinpalDriver = defineDriver({ const { merchantId, sandbox } = ctx; const links = getLinks(ctx.links, sandbox ?? false); - if (status !== 'OK') { - throw new PaymentException(); - } + if (status !== 'OK') throw new GatewayFailureError(); const response = await axios.post( links.verify, @@ -96,11 +100,10 @@ export const createZarinpalDriver = defineDriver({ if (!Array.isArray(errors)) { // There are errors (`errors` is an object) - const { code } = errors; - throw new VerificationException(API.verifyErrors[code.toString()]); + throwError(errors.code.toString()); } - throw new VerificationException(); + throw new GatewayFailureError(); }, }); diff --git a/src/drivers/zibal/api.ts b/src/drivers/zibal/api.ts index 8a480ec..4150251 100644 --- a/src/drivers/zibal/api.ts +++ b/src/drivers/zibal/api.ts @@ -231,3 +231,6 @@ export const verifyErrors: Record = { '202': 'سفارش پرداخت نشده یا ناموفق بوده است. جهت اطلاعات بیشتر جدول وضعیت‌ها را مطالعه کنید.', '203': 'trackId نامعتبر می‌باشد.', }; + +export const IPGConfigErrors = ['102', '103', '104', '201', '105', '106', '113', '203']; +export const IPGUserErrors = ['202', '3', '4', '5', '6', '7', '8', '9', '10', '12']; diff --git a/src/drivers/zibal/zibal.spec.ts b/src/drivers/zibal/zibal.spec.ts index 36358d5..cd87939 100644 --- a/src/drivers/zibal/zibal.spec.ts +++ b/src/drivers/zibal/zibal.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Receipt } from '../../driver'; -import { RequestException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; import { createZibalDriver, ZibalDriver } from './zibal'; @@ -31,6 +31,18 @@ describe('Zibal Driver', () => { }); it('throws payment errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + result: 1000, + message: 'some error', + trackId: 1234, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(GatewayFailureError); + }); + + it('throws payment bad config errors accordingly', async () => { const serverResponse: API.RequestPaymentRes = { result: 102, message: 'some error', @@ -39,7 +51,19 @@ describe('Zibal Driver', () => { mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); - await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(RequestException); + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(BadConfigError); + }); + + it('throws payment user errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + result: 6, + message: 'some error', + trackId: 1234, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); + + await expect(driver.request({ amount: 2000, callbackUrl: 'asd' })).rejects.toThrow(UserError); }); it('verifies the purchase correctly', async () => { diff --git a/src/drivers/zibal/zibal.ts b/src/drivers/zibal/zibal.ts index a77fd1d..1511818 100644 --- a/src/drivers/zibal/zibal.ts +++ b/src/drivers/zibal/zibal.ts @@ -1,11 +1,18 @@ import axios from 'axios'; import { z } from 'zod'; import { defineDriver } from '../../driver'; -import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import { BadConfigError, GatewayFailureError, UserError } from '../../exceptions'; import * as API from './api'; const getMerchantId = (merchantId: string, sandbox: boolean) => (sandbox ? 'zibal' : merchantId); +const throwError = (errorCode: string) => { + const message = API.purchaseErrors[errorCode] ?? API.callbackErrors[errorCode] ?? API.verifyErrors[errorCode]; + if (API.IPGConfigErrors.includes(errorCode)) throw new BadConfigError({ message, isIPGError: true, code: errorCode }); + if (API.IPGUserErrors.includes(errorCode)) throw new UserError({ message, code: errorCode }); + throw new GatewayFailureError({ message, code: errorCode }); +}; + export const createZibalDriver = defineDriver({ schema: { config: z.object({ @@ -48,7 +55,7 @@ export const createZibalDriver = defineDriver({ const { result, trackId } = response.data; if (result !== 100) { - throw new RequestException(API.purchaseErrors[result.toString()]); + throwError(result.toString()); } return { @@ -62,7 +69,7 @@ export const createZibalDriver = defineDriver({ const { merchantId, sandbox, links } = ctx; if (success.toString() === '0') { - throw new PaymentException(API.callbackErrors[status]); + throwError(status.toString()); } const response = await axios.post(links.verify, { @@ -73,7 +80,7 @@ export const createZibalDriver = defineDriver({ const { result } = response.data; if (result !== 100) { - throw new VerificationException(API.verifyErrors[result.toString()]); + throwError(result.toString()); } return { diff --git a/src/exceptions.ts b/src/exceptions.ts index 58c4bf6..675457a 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -1,48 +1,57 @@ -export class BasePaymentException extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, BasePaymentException.prototype); - } -} +type MonoPayErrorConfig = { + isIPGError: boolean; + isSafeToDisplay: boolean; + message?: string; + code?: string; +}; -/** - * Error in the requesting stage - */ -export class RequestException extends BasePaymentException { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, RequestException.prototype); +export abstract class MonopayError extends Error { + /** + * Determines whether the error was thrown by the IPG or by the application itself + */ + readonly isIPGError: boolean; + /** + * Determines whether the error exposes sensitive information or not + */ + readonly isSafeToDisplay: boolean; + /** + * Contains the IPG error code (if IPG provides any) + */ + readonly code?: string; + constructor(options: MonoPayErrorConfig) { + super(options.message); + this.isIPGError = options.isIPGError; + this.isSafeToDisplay = options.isSafeToDisplay; + this.code = options.code; } } /** - * Error in the paying stage - * - * You can show this error message to your end user + * Denotes an error caused by developer configuration */ -export class PaymentException extends BasePaymentException { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, PaymentException.prototype); +export class BadConfigError extends MonopayError { + constructor(details: { message: string; code?: string; isIPGError: boolean }) { + const { message, code, isIPGError } = details; + super({ message, code, isIPGError, isSafeToDisplay: false }); } } /** - * Error in the verification stage + * Denotes an error caused by end user */ -export class VerificationException extends BasePaymentException { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, VerificationException.prototype); +export class UserError extends MonopayError { + constructor(details?: { message?: string; code?: string }) { + const { message, code } = details ?? {}; + super({ message, code, isIPGError: true, isSafeToDisplay: true }); } } /** - * Error when the configuration has problems + * Denotes an error either caused by a failure from gateway or an unrecognizable reason */ -export class BadConfigException extends BasePaymentException { - constructor(errors: string[]) { - super(errors.join(',\n')); - Object.setPrototypeOf(this, BadConfigException.prototype); +export class GatewayFailureError extends MonopayError { + constructor(details?: { message?: string; code?: string }) { + const { message, code } = details ?? {}; + super({ message, code, isIPGError: true, isSafeToDisplay: false }); } } diff --git a/src/utils/safeParse.ts b/src/utils/safeParse.ts new file mode 100644 index 0000000..31f4e2c --- /dev/null +++ b/src/utils/safeParse.ts @@ -0,0 +1,9 @@ +import { BadConfigError } from '../exceptions'; +import { ZodSchema } from 'zod'; + +export const safeParse = (parser: ZodSchema, data: T): T => { + const parsed = parser.safeParse(data); + if (parsed.success) return parsed.data; + const message = parsed.error.errors.map((err) => `${err.path[0]}: ${err.message}`).join('\n'); + throw new BadConfigError({ message, isIPGError: false }); +};