From 8c06ed3ac0c8230b46a81dfca33da69c8599e26d Mon Sep 17 00:00:00 2001 From: alitnk Date: Sun, 17 Oct 2021 12:51:43 +0330 Subject: [PATCH] Added Parsian driver --- src/drivers.ts | 5 + src/drivers/parsian/api.ts | 182 +++++++++++++++++++++++++++++++++++ src/drivers/parsian/index.ts | 94 ++++++++++++++++++ test/drivers/parsian.spec.ts | 85 ++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 src/drivers/parsian/api.ts create mode 100644 src/drivers/parsian/index.ts create mode 100644 test/drivers/parsian.spec.ts diff --git a/src/drivers.ts b/src/drivers.ts index 11bb1af..400128b 100644 --- a/src/drivers.ts +++ b/src/drivers.ts @@ -5,6 +5,8 @@ import { IdPay } from './drivers/idpay'; import * as IdPayAPI from './drivers/idpay/api'; import { NextPay } from './drivers/nextpay'; import * as NextPayAPI from './drivers/nextpay/api'; +import { Parsian } from './drivers/parsian'; +import * as ParsianAPI from './drivers/parsian/api'; import { Payir } from './drivers/payir'; import * as PayirAPI from './drivers/payir/api'; import { PayPing } from './drivers/payping'; @@ -21,6 +23,7 @@ import * as ZibalAPI from './drivers/zibal/api'; export { Behpardakht } from './drivers/behpardakht'; export { IdPay } from './drivers/idpay'; export { NextPay } from './drivers/nextpay'; +export { Parsian } from './drivers/parsian'; export { PayPing } from './drivers/payping'; export { Sadad } from './drivers/sadad'; export { Saman } from './drivers/saman'; @@ -31,6 +34,7 @@ interface ConfigMap { idpay: IdPayAPI.Config; nextpay: NextPayAPI.Config; payir: PayirAPI.Config; + parsian: ParsianAPI.Config; payping: PayPingAPI.Config; sadad: SadadAPI.Config; saman: SamanAPI.Config; @@ -47,6 +51,7 @@ const drivers = { idpay: IdPay, nextpay: NextPay, payir: Payir, + parsian: Parsian, payping: PayPing, sadad: Sadad, saman: Saman, diff --git a/src/drivers/parsian/api.ts b/src/drivers/parsian/api.ts new file mode 100644 index 0000000..87baefd --- /dev/null +++ b/src/drivers/parsian/api.ts @@ -0,0 +1,182 @@ +import * as t from 'io-ts'; +import { BaseReceipt, LinksObject, tBaseRequestOptions, tBaseVerifyOptions } from '../../types'; + +/* + * Parsian's API + * Currency: IRR + * + * Docs: https://miladworkshop.ir/blog/19-parsian-gateway-sample-code.html + */ + +export const links: LinksObject = { + default: { + REQUEST: 'https://pec.shaparak.ir/NewIPGServices/Sale/SaleService.asmx?wsdl', + VERIFICATION: 'https://pec.shaparak.ir/NewIPGServices/Confirm/ConfirmService.asmx?wsdl', + PAYMENT: '‫‪https://pec.shaparak.ir/NewIPG/', + }, +}; + +/** + * Request of `SalePaymentRequest` + */ +export interface RequestPaymentReq { + /** + * فروشنده پین + */ + LoginAccount: string; + + /** + * پرداخت مبلغ + */ + Amount: number; + + /** + * شماره سفارش - بایت یکتا باشد و یکتایی آن از جانب بانک نیز کنترل شود + */ + OrderId: number; + + /** + * صفحه بازگشت مشتری به وب سایت پذیرنده، ‌پس از انجام عمل پرداخت. + */ + CallBackUrl: string; + + /** + * داده اضافی + */ + AdditionalData: string; +} + +/** + * Response of `SalePaymentRequest` + */ +export interface RequestPaymentRes { + /** + * شماره درخواست در دروازه پرداخت که یک شماره تصادفی و یکتا برای تمامی عملیات تراکنش می باشد و فروشگاه ملزم به ثبت و نگهداری این کد است + */ + Token?: number | string; + + /** + * کد وضعیت در عملیات موفق صفر است + */ + Status: number; +} + +/** + * NOTE: this gets sent on POST method + */ +export interface CallbackParams { + Token: number | string; + status: number | string; + OrderId: number | string; + TerminalNo: number | string; + RRN: number | string; + HashCardNumber: string; + Amount: number; +} + +/** + * Request of `ConfirmPayment` + */ +export interface VerifyPaymentReq { + /** + * پذیرنده شناسایی کد + */ + LoginAccount: string; + + /** + * پرداخت دروازه در درخواست شماره + */ + Token: number | string; +} + +/** + * Response of `ConfirmPayment` + */ +export interface VerifyPaymentRes { + /** + * کد وضعیت عملیات که در صورت موفقیت صفر است + */ + Status: number | string; + + /** + * شماره مکرجع + */ + RRN: number; + + /** + * شماره کارت کاربر به صورت ماسک شده + */ + CardNumberMasked: string; + + /** + * شماره درخواست تراکنش دروازه پرداخت پارسیان + */ + Token: number | string; +} + +/** + * Request of `ReversalRequest` + */ +export type ReversalPaymentReq = VerifyPaymentReq; + +/** + * Response of `ReversalRequest` + */ +export interface ReversalPaymentRes { + /** + * کد وضعیت عملیات که در صورت موفقیت صفر است + */ + Status: string; + + /** + * شماره درخواست تراکنش دروازه پرداخت پارسیان + */ + Token: number | string; +} + +// export const errors: ErrorList = { +// // UnkownError +// '-32768': 'خطای ناشناخته رخ داده است', +// // Payment RequestIsNotEligibleToReversal +// '-1552': 'برگشت تراکنش مجاز نمی باشد', +// // Payment RequestsAlreadIsReversed +// '-1551': 'برگشت تراکت قبلا انجام شده است', +// // PaymentequestStatusIsNotReversalable +// '-1550': 'برگشت تراکش در وضعیت جاری امکان پذیر نمی باشد', +// // MaxAllowedTimeToReversalHasExceeded +// '-1549': 'زمان مجاز برای در خواست برگشت تراکنش به اتمام رسیده است', +// // BillPaymentRequestServiceFailed +// '-1548': 'فراخوانی سرویس درخواست پرداخت قبض ناموفق بود ', +// // InvalidConfigmRequestService +// '-1540': 'تایید تراکنش ناموفق می باشد', +// //TopupChargeServiceTopupChargeRequestFailed +// '-1536': 'فراخوانی سرویس درخواست شارژ تاپ آپ الاموفق بود', +// // PaymentIsAlreadyConfirmed +// '-1533': 'تراکنش قلا تایید شده است', +// // MerchantHasConfirmedPaymentRequest +// '-1532': 'تراکنش از سوی پذیرنده تایید شد', +// // CannotConfirmNonSuccessfulPayment +// '-1531': 'تایید تراکنش ناموفق امکان پذیر نمی باشد', +// // there's like 6 more pages of these errors +// // TODO import the errors? +// }; + +/* + * Package's API + */ + +export const tConfig = t.interface({ + merchantId: t.string, +}); + +export type Config = t.TypeOf; + +export const tRequestOptions = t.intersection([t.partial({}), tBaseRequestOptions]); + +export type RequestOptions = t.TypeOf; + +export const tVerifyOptions = t.intersection([t.interface({}), tBaseVerifyOptions]); + +export type VerifyOptions = t.TypeOf; + +export type Receipt = BaseReceipt; diff --git a/src/drivers/parsian/index.ts b/src/drivers/parsian/index.ts new file mode 100644 index 0000000..443522c --- /dev/null +++ b/src/drivers/parsian/index.ts @@ -0,0 +1,94 @@ +import soap from 'soap'; +import { Driver } from '../../driver'; +import { PaymentException, RequestException, VerificationException } from '../../exceptions'; +import * as API from './api'; + +export class Parsian extends Driver { + constructor(config: API.Config) { + super(config, API.tConfig); + } + + protected links = API.links; + + requestPayment = async (options: API.RequestOptions) => { + options = this.getParsedData(options, API.tRequestOptions); + + const { amount, callbackUrl, description } = options; + const { merchantId } = this.config; + const client = await soap.createClientAsync(this.getLinks().REQUEST); + + const requestFields: API.RequestPaymentReq = { + Amount: amount, + CallBackUrl: callbackUrl, + AdditionalData: description || '', + LoginAccount: merchantId, + OrderId: this.generateId(), + }; + + const response: API.RequestPaymentRes = client.SalePaymentRequest(requestFields); + + const { Status, Token } = response; + if (Status.toString() !== '0' || typeof Token === 'undefined') { + throw new RequestException('خطایی در درخواست پرداخت به‌وجود آمد'); + } + + return this.makeRequestInfo(Token, 'GET', this.getLinks().PAYMENT, { + Token, + }); + }; + + verifyPayment = async (_options: API.VerifyOptions, params: API.CallbackParams): Promise => { + const { Token, status } = params; + const { merchantId } = this.config; + + if (status.toString() !== '0') { + throw new PaymentException('تراکنش توسط کاربر لغو شد.'); + } + + const soapClient = await soap.createClientAsync(this.getLinks().VERIFICATION); + + const requestFields: API.VerifyPaymentReq = { + LoginAccount: merchantId, + Token: +Token, + }; + + // 1. Verify + const verifyResponse: API.VerifyPaymentRes = soapClient.ConfirmPayment(requestFields); + + const { CardNumberMasked, RRN, Status } = verifyResponse; + if (!(Status.toString() === '0' && RRN > 0)) { + const reversalRequestFields: API.ReversalPaymentReq = requestFields; + const reversalResponse: API.ReversalPaymentRes = soapClient.ReversalRequest(reversalRequestFields); + if (reversalResponse.Status !== '0') { + throw new VerificationException('خطایی در تایید پرداخت به‌وجود آمد و مبلغ بازگشته نشد.'); + } + throw new VerificationException('خطایی در تایید پرداخت به‌وجود آمد'); + } + + return { + transactionId: RRN, + cardPan: CardNumberMasked, + raw: verifyResponse, + }; + }; + + /** + * YYYYMMDD + */ + dateFormat(date = new Date()) { + const yyyy = date.getFullYear(); + const mm = date.getMonth() + 1; + const dd = date.getDate(); + return yyyy.toString() + mm.toString() + dd.toString(); + } + + /** + * HHMMSS + */ + timeFormat(date = new Date()) { + const hh = date.getHours(); + const mm = date.getMonth(); + const ss = date.getSeconds(); + return hh.toString() + mm.toString() + ss.toString(); + } +} diff --git a/test/drivers/parsian.spec.ts b/test/drivers/parsian.spec.ts new file mode 100644 index 0000000..eeb1ccc --- /dev/null +++ b/test/drivers/parsian.spec.ts @@ -0,0 +1,85 @@ +import { Parsian } from '../../src/drivers/parsian'; +import * as API from '../../src/drivers/parsian/api'; +import { RequestException } from '../../src/exceptions'; +import { getPaymentDriver } from '../../src/drivers'; + +const mockSoapClient: any = {}; +jest.mock('soap', () => ({ + createClientAsync: async () => mockSoapClient, +})); + +// const mockedSoap = soap as jest.Mocked; +describe('Parsian Driver', () => { + it('returns the correct payment url', async () => { + const serverResponse: API.RequestPaymentRes = { + Token: 123, + Status: 0, + }; + + mockSoapClient.SalePaymentRequest = () => serverResponse; + + const driver = getPaymentDriver('parsian', { + merchantId: 'merchant-id', + }); + + expect( + typeof ( + await driver.requestPayment({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + }) + ).url + ).toBe('string'); + }); + + it('throws payment errors accordingly', async () => { + const serverResponse: API.RequestPaymentRes = { + Status: 1, + }; + + mockSoapClient.SalePaymentRequest = () => serverResponse; + + const driver = getPaymentDriver('parsian', { + merchantId: 'merchant-id', + }); + + await expect( + async () => + await driver.requestPayment({ + amount: 20000, + callbackUrl: 'https://mysite.com/callback', + }) + ).rejects.toThrow(RequestException); + }); + + it('verifies the purchase correctly', async () => { + const serverResponse: API.VerifyPaymentRes = { + RRN: 123456789, + CardNumberMasked: '1234-****-****-1234', + Status: 0, + Token: 12345, + }; + const callbackParams: API.CallbackParams = { + Amount: 20000, + HashCardNumber: 'hashed-card', + OrderId: 1234, + RRN: 123456789, + TerminalNo: 22, + Token: 12345, + status: 0, + }; + + const expectedResult: API.Receipt = { transactionId: 123456789, raw: serverResponse }; + + mockSoapClient.ConfirmPayment = () => serverResponse; + mockSoapClient.ReversalRequest = () => serverResponse; + + const driver = getPaymentDriver('parsian', { + merchantId: 'merchant-id', + }); + + expect(await (await driver.verifyPayment({ amount: 2000 }, callbackParams)).transactionId).toBe( + expectedResult.transactionId + ); + }); +});