Skip to content

Commit

Permalink
feat(drivers): Vandar payment provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Scrip7 committed Mar 31, 2022
1 parent 83dfc83 commit 8ec5976
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/drivers.ts
Expand Up @@ -15,6 +15,8 @@ import { Sadad } from './drivers/sadad';
import * as SadadAPI from './drivers/sadad/api';
import { Saman } from './drivers/saman';
import * as SamanAPI from './drivers/saman/api';
import { Vandar } from './drivers/vandar';
import * as VandarAPI from './drivers/vandar/api';
import { Zarinpal } from './drivers/zarinpal';
import * as ZarinpalAPI from './drivers/zarinpal/api';
import { Zibal } from './drivers/zibal';
Expand All @@ -28,6 +30,7 @@ export { Payir } from './drivers/payir';
export { PayPing } from './drivers/payping';
export { Sadad } from './drivers/sadad';
export { Saman } from './drivers/saman';
export { Vandar } from './drivers/vandar';
export { Zarinpal } from './drivers/zarinpal';
export { Zibal } from './drivers/zibal';
interface ConfigMap {
Expand All @@ -39,6 +42,7 @@ interface ConfigMap {
payping: PayPingAPI.Config;
sadad: SadadAPI.Config;
saman: SamanAPI.Config;
vandar: VandarAPI.Config;
zarinpal: ZarinpalAPI.Config;
zibal: ZibalAPI.Config;
}
Expand All @@ -56,6 +60,7 @@ const drivers = {
payping: PayPing,
sadad: Sadad,
saman: Saman,
vandar: Vandar,
zarinpal: Zarinpal,
zibal: Zibal,
};
Expand Down
184 changes: 184 additions & 0 deletions src/drivers/vandar/api.ts
@@ -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>;
81 changes: 81 additions & 0 deletions src/drivers/vandar/index.ts
@@ -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;
}
}
55 changes: 55 additions & 0 deletions test/drivers/vandar.spec.ts
@@ -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
);
});
});

0 comments on commit 8ec5976

Please sign in to comment.