Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(drivers): Vandar payment provider
- Loading branch information
Showing
4 changed files
with
325 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,184 @@ | ||
import * as t from 'io-ts'; | ||
import { BaseReceipt, LinksObject, tBaseRequestOptions, tBaseVerifyOptions } from '../../types'; | ||
|
||
/* | ||
* Vandar's API | ||
* Currency: IRR | ||
*/ | ||
|
||
export const links: LinksObject = { | ||
default: { | ||
REQUEST: 'https://ipg.vandar.io/api/v3/send', | ||
VERIFICATION: 'https://ipg.vandar.io/api/v3/verify', | ||
PAYMENT: 'https://ipg.vandar.io/v3/', | ||
}, | ||
}; | ||
|
||
export interface RequestPaymentReq { | ||
/** | ||
* این کلید بعد از ساخت درگاه پرداخت صادر میشود. برای دریافت این کلید به داشبورد وندار مراجعه کنید | ||
*/ | ||
api_key: string; | ||
/** | ||
* مبلغ تراکنش به صورت ریالی و بزرگتر یا مساوی 1000 | ||
*/ | ||
amount: number; | ||
/** | ||
* باید با آدرس درگاه پرداخت تایید شده در وندار بر روی یک دامنه باشد | ||
*/ | ||
callback_url: string; | ||
/** | ||
* شماره موبایل | ||
* (جهت نمایش کارت های خریدار به ایشان و نمایش درگاه موبایلی) | ||
*/ | ||
mobile_number?: string; | ||
/** | ||
* شماره فاکتور شما | ||
*/ | ||
factorNumber?: string; | ||
/** | ||
* توضیحات (حداکثر 255 کاراکتر) | ||
*/ | ||
description?: string; | ||
/** | ||
* شماره کارت معتبر | ||
* | ||
* (در صورت ارسال شماره کارت، کاربر فقط با همان شماره کارت قابلیت پرداخت خواهد داشت.) | ||
*/ | ||
valid_card_number?: string; | ||
/** | ||
* یک یادداشت که در داشبورد شما روی تراکنش نمایش داده می شود. | ||
*/ | ||
comment?: string; | ||
} | ||
|
||
export interface RequestPaymentRes { | ||
/** | ||
* مقدار 0 و 1 دارد که نشان دهنده موفقیت آمیز بودن درخواست است | ||
*/ | ||
status: 1 | 0; | ||
/** | ||
* یک رشته از حروف و اعداد است با طول متغییر، که توکن پرداخت است و باید سمت پذیرنده نگهداری شود. | ||
*/ | ||
token?: string; | ||
errors?: string[]; | ||
} | ||
|
||
export interface CallbackParams { | ||
token: string; | ||
/** | ||
* TODO: Fail status is not documented! | ||
*/ | ||
payment_status: 'OK' | string; | ||
} | ||
|
||
export interface VerifyPaymentReq { | ||
/** | ||
* این کلید بعد از ساخت درگاه پرداخت صادر میشود. برای دریافت این کلید به داشبورد وندار مراجعه کنید | ||
*/ | ||
api_key: string; | ||
/** | ||
* همان توکن پرداختی که در مرحله یک دریافت کردید و در این مرحله از به صورت انتهای آدرس بازگشتی اضافه شده است. | ||
*/ | ||
token: string; | ||
} | ||
|
||
export interface VerifyPaymentRes { | ||
status: number; | ||
/** | ||
* مبلغ تراکنش که ممکن است با مبلغ تراکنشی که در مرحله اول ارسال کرده باشید متفاوت باشد. | ||
* اگر کارمزد تراکنش بر عهده پرداخت کننده باشد. مبلغ کارمزد هم به مبلغ تراکنش ارسالی از سمت شما اضافه شده است. | ||
*/ | ||
amount?: string; | ||
/** | ||
* مبلغی که بر اساس این تراکنش کیف پول شما بالا رفته است. | ||
*/ | ||
realAmount?: number; | ||
/** | ||
* کارمزد تراکنش | ||
*/ | ||
wage?: string; | ||
/** | ||
* شناسه یکتای پرداخت که برای پیگیری تراکنش از وندار مورد استفاده قرار میگیرد. | ||
*/ | ||
transId?: number; | ||
/** | ||
* شماره فاکتوری که شما در مرحله اول ارسال کردید. | ||
*/ | ||
factorNumber?: string; | ||
/** | ||
* شماره موبایل پرداخت کننده که در مرحله اول ارسال کردید. | ||
*/ | ||
mobile?: string; | ||
/** | ||
* توضیحاتی که شما در مرحله اول ارسال کردید. | ||
*/ | ||
description?: string; | ||
/** | ||
* ماسکه شده شماره کارت پرداخت کننده. | ||
*/ | ||
cardNumber?: string; | ||
/** | ||
* تاریخ انجام تراکنش. | ||
*/ | ||
paymentDate?: string; | ||
/** | ||
* هش شماره کارت که با الگوریتم SHA256 هش شده است. | ||
*/ | ||
cid?: null | string; | ||
/** | ||
* وضعیت تراکنش | ||
*/ | ||
message?: string; | ||
|
||
errors?: string[]; | ||
} | ||
|
||
/* | ||
* Package's API | ||
*/ | ||
|
||
export const tConfig = t.intersection([ | ||
t.partial({}), | ||
t.interface({ | ||
api_key: t.string, | ||
}), | ||
]); | ||
|
||
export type Config = t.TypeOf<typeof tConfig>; | ||
|
||
export const tRequestOptions = t.intersection([ | ||
t.partial({ | ||
mobile_number: t.string, | ||
factorNumber: t.string, | ||
description: t.string, | ||
valid_card_number: t.string, | ||
comment: t.string, | ||
}), | ||
tBaseRequestOptions, | ||
]); | ||
|
||
export type RequestOptions = t.TypeOf<typeof tRequestOptions>; | ||
|
||
export const tVerifyOptions = t.intersection([ | ||
t.partial({ | ||
status: t.number, | ||
// amount: t.string, | ||
realAmount: t.number, | ||
wage: t.string, | ||
transId: t.number, | ||
factorNumber: t.string, | ||
mobile: t.string, | ||
description: t.string, | ||
cardNumber: t.string, | ||
paymentDate: t.string, | ||
cid: t.string, | ||
message: t.string, | ||
errors: t.array(t.string), | ||
}), | ||
tBaseVerifyOptions, | ||
]); | ||
|
||
export type VerifyOptions = t.TypeOf<typeof tVerifyOptions>; | ||
|
||
export type Receipt = BaseReceipt<CallbackParams>; |
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,81 @@ | ||
import axios from 'axios'; | ||
import { Driver } from '../../driver'; | ||
import { PaymentException, RequestException, VerificationException } from '../../exceptions'; | ||
import * as API from './api'; | ||
|
||
export class Vandar 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, ...otherOptions } = options; | ||
const { api_key } = this.config; | ||
|
||
let response = await axios.post<API.RequestPaymentReq, { data: API.RequestPaymentRes }>(this.getLinks().REQUEST, { | ||
api_key, | ||
amount: amount, | ||
callback_url: callbackUrl, | ||
...otherOptions, | ||
}); | ||
const { token, errors } = response.data; | ||
|
||
if (token?.length) { | ||
return this.makeRequestInfo(token, 'GET', this.getLinks().PAYMENT + response.data.token); | ||
} | ||
|
||
if (errors?.length) { | ||
throw new RequestException(errors.join('\n')); | ||
} | ||
|
||
throw new RequestException(); | ||
}; | ||
|
||
verifyPayment = async (options: API.VerifyOptions, params: API.CallbackParams): Promise<API.Receipt> => { | ||
options = this.getParsedData(options, API.tVerifyOptions); | ||
|
||
const { token, payment_status } = params; | ||
// const { amount } = options; | ||
const { api_key } = this.config; | ||
|
||
if (payment_status !== 'OK') { | ||
throw new PaymentException(); | ||
} | ||
|
||
const response = await axios.post<API.VerifyPaymentReq, { data: API.VerifyPaymentRes }>( | ||
this.getLinks().VERIFICATION, | ||
{ | ||
api_key, | ||
token, | ||
}, | ||
{} | ||
); | ||
const { errors } = response.data; | ||
|
||
if ([0, 1].includes(response.data.status) && response.data.transId !== undefined) { | ||
return { | ||
transactionId: response.data.transId, | ||
cardPan: response.data.cardNumber, | ||
raw: { | ||
token, | ||
payment_status, | ||
}, | ||
}; | ||
} | ||
|
||
if (errors?.length) { | ||
// There are errors (`errors` is an object) | ||
throw new VerificationException(errors.join('\n')); | ||
} | ||
|
||
throw new VerificationException(); | ||
}; | ||
|
||
protected getLinks() { | ||
return this.links.default; | ||
} | ||
} |
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,55 @@ | ||
import axios from 'axios'; | ||
import { getPaymentDriver } from '../../src/drivers'; | ||
import { Vandar } from '../../src/drivers/vandar'; | ||
import * as API from '../../src/drivers/vandar/api'; | ||
import { RequestException } from '../../src/exceptions'; | ||
|
||
jest.mock('axios'); | ||
|
||
const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
describe('Vandar Driver', () => { | ||
it('returns the correct payment url', async () => { | ||
const serverResponse: API.RequestPaymentRes = { | ||
status: 1, | ||
token: '123', | ||
}; | ||
|
||
mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); | ||
|
||
const driver = getPaymentDriver<Vandar>('vandar', { api_key: '1234' }); | ||
|
||
expect(typeof (await driver.requestPayment({ amount: 2000, callbackUrl: 'asd' })).url).toBe('string'); | ||
}); | ||
|
||
it('throws payment errors accordingly', async () => { | ||
const serverResponse: API.RequestPaymentRes = { | ||
status: 0, | ||
errors: ['A', 'B'], | ||
}; | ||
|
||
mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); | ||
|
||
const driver = getPaymentDriver<Vandar>('vandar', { api_key: '2134' }); | ||
|
||
await expect( | ||
async () => await driver.requestPayment({ amount: 2000, callbackUrl: 'https://example.com' }) | ||
).rejects.toThrow(RequestException); | ||
}); | ||
|
||
it('verifies the purchase correctly', async () => { | ||
const serverResponse: API.VerifyPaymentRes = { | ||
status: 1, | ||
transId: 201, | ||
errors: [], | ||
}; | ||
const expectedResult: API.Receipt = { transactionId: 201, raw: serverResponse as any }; | ||
|
||
mockedAxios.post.mockResolvedValueOnce({ data: serverResponse }); | ||
|
||
const driver = getPaymentDriver<Vandar>('vandar', { api_key: '2134' }); | ||
|
||
expect((await driver.verifyPayment({ amount: 2000 }, { token: '2000', payment_status: 'OK' })).transactionId).toBe( | ||
expectedResult.transactionId | ||
); | ||
}); | ||
}); |