Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof tConfig>; | ||
|
||
export const tRequestOptions = t.intersection([t.partial({}), tBaseRequestOptions]); | ||
|
||
export type RequestOptions = t.TypeOf<typeof tRequestOptions>; | ||
|
||
export const tVerifyOptions = t.intersection([t.interface({}), tBaseVerifyOptions]); | ||
|
||
export type VerifyOptions = t.TypeOf<typeof tVerifyOptions>; | ||
|
||
export type Receipt = BaseReceipt<VerifyPaymentRes>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<API.Config> { | ||
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<API.Receipt> => { | ||
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof soap>; | ||
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>('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>('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>('parsian', { | ||
merchantId: 'merchant-id', | ||
}); | ||
|
||
expect(await (await driver.verifyPayment({ amount: 2000 }, callbackParams)).transactionId).toBe( | ||
expectedResult.transactionId | ||
); | ||
}); | ||
}); |