diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..260ef7c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# CHANGELOG + +## 2019-12-29 + +- [#16](https://github.com/xendit/xendit-node/pull/16) (feature) Recurring Payments +- [#18](https://github.com/xendit/xendit-node/pull/18) (feature) Payouts +- [#20](https://github.com/xendit/xendit-node/pull/20) (feature) EWallets + +## 2019-12-28 + +- [#19](https://github.com/xendit/xendit-node/pull/19) Add linting for ts files + +## 2019-12-26 + +- [#15](https://github.com/xendit/xendit-node/pull/15) Remove tokenization & authentication from Card + +## 2019-12-16 + +- [#13](https://github.com/xendit/xendit-node/pull/13) Add integration tests + +## 2019-11-28 + +- (feature) Invoices + +## 2019-11-26 + +- (bugfix) `createdFixedVA` on nil expirationDate +- (bugfix) `updateFixedVA` on nul expirationDate + +## 2019-11-03 + +- (feature) Credit Cards +- (feature) Virtual Accounts +- (feature) Disbursements +- (feature) Batch Disbursements diff --git a/README.md b/README.md index 9af5382..9cd83db 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ For PCI compliance to be maintained, tokenization of credt cards info should be + [Methods](#methods-4) * [Payout Services](#payout-services) + [Methods](#methods-5) + * [EWallet Services](#ewallet-services) + + [Methods](#methods-6) - [Contributing](#contributing) @@ -516,6 +518,60 @@ p.getPayout(data: { id: string }) p.voidPayout(data: { id: string }) ``` +### EWallet Services + +Instanitiate EWallet service using constructor that has been injected with Xendit keys + +```js +const { EWallet } = x; +const ewalletSpecificOptions = {}; +const ew = new EWallet(ewalletSpecificOptions); +``` + +Example: Create an ewallet payment + +```js +ew.createPayment({ + externalID: 'my-ovo-payment', + amount: 1, + phone: '081234567890', + ewalletType: EWallet.Type.OVO, + }) + .then(r => { + console.log('create ewallet payment detail:', r); + return r; + }) +``` + +#### Methods + +- Create an ewallet payment +```ts +ew.createPayment(data: { + externalID: string; + amount: number; + phone?: string; + expirationDate?: Date; + callbackURL?: string; + redirectURL?: string; + items?: Array<{ + id: string; + name: string; + price: number; + quantity: number; + }>; + ewalletType: CreateSupportWalletTypes; +}) +``` + +- Get an ewallet Payment Status +```ts +ew.getPayment(data: { + externalID: string: + ewalletType: GetSupportWalletTypes; +}) +``` + ## Contributing Running test suite diff --git a/examples/ewallet.js b/examples/ewallet.js new file mode 100644 index 0000000..2a8bad4 --- /dev/null +++ b/examples/ewallet.js @@ -0,0 +1,29 @@ +const x = require('./xendit'); + +const EWallet = x.EWallet; +const ew = new EWallet({}); + +ew.createPayment({ + externalID: new Date(), + amount: 1, + phone: '081234567890', + ewalletType: EWallet.Type.OVO, +}) + .then(r => { + console.log('create payment detail:', r); // eslint-disable-line no-console + return r; + }) + .then(({ external_id, ewallet_type }) => + ew.ovo.getPaymentStatusByExtID({ + externalID: external_id, + ewalletType: ewallet_type, + }), + ) + .then(r => { + console.log('EWallet payment detail:', r); // eslint-disable-line no-console + return r; + }) + .catch(e => { + console.error(e); // eslint-disable-line no-console + process.exit(1); + }); diff --git a/src/ewallet/ewallet.d.ts b/src/ewallet/ewallet.d.ts new file mode 100644 index 0000000..33494e6 --- /dev/null +++ b/src/ewallet/ewallet.d.ts @@ -0,0 +1,45 @@ +import { XenditOptions } from '../xendit_opts'; + +enum CreateSupportWalletTypes { + OVO = 'OVO', + Dana = 'DANA', + Linkaja = 'LINKAJA', +} + +enum GetSupportWalletTypes { + OVO = 'OVO', + Dana = 'DANA', +} + +interface PaymentItem { + id: string; + name: string; + price: number; + quantity: number; +} + +export = class EWallet { + constructor({}); + static _constructorWithInjectedXenditOpts: ( + opts: XenditOptions, + ) => typeof EWallet; + static Type: { + OVO: string; + Dana: string; + LinkAja: string; + }; + createPayment(data: { + externalID: string; + amount: number; + phone?: string; + expirationDate?: Date; + callbackURL?: string; + redirectURL?: string; + items?: PaymentItem[]; + ewalletType: CreateSupportWalletTypes; + }): Promise; + getPayment(data: { + externalID: string; + ewalletType: GetSupportWalletTypes; + }): Promise; +}; diff --git a/src/ewallet/ewallet.js b/src/ewallet/ewallet.js new file mode 100644 index 0000000..2a320ea --- /dev/null +++ b/src/ewallet/ewallet.js @@ -0,0 +1,117 @@ +const { + promWithJsErr, + Auth, + Validate, + fetchWithHTTPErr, + queryStringWithoutUndefined, +} = require('../utils'); +const errors = require('../errors'); + +const EWALLET_PATH = '/ewallets'; + +function EWallet(options) { + let aggOpts = options; + if (EWallet._injectedOpts && Object.keys(EWallet._injectedOpts).length > 0) { + aggOpts = Object.assign({}, options, EWallet._injectedOpts); + } + + this.opts = aggOpts; + this.API_ENDPOINT = this.opts.xenditURL + EWALLET_PATH; +} + +EWallet._injectedOpts = {}; +EWallet._constructorWithInjectedXenditOpts = function(options) { + EWallet._injectedOpts = options; + return EWallet; +}; +EWallet.Type = { + OVO: 'OVO', + Dana: 'DANA', + LinkAja: 'LINKAJA', +}; + +EWallet.prototype.createPayment = function(data) { + return promWithJsErr((resolve, reject) => { + let compulsoryFields = ['ewalletType']; + + if (data.ewalletType) { + switch (data.ewalletType) { + case EWallet.Type.OVO: + compulsoryFields = ['externalID', 'amount', 'phone', 'ewalletType']; + break; + case EWallet.Type.Dana: + compulsoryFields = [ + 'externalID', + 'amount', + 'callbackURL', + 'redirectURL', + 'ewalletType', + ]; + break; + case EWallet.Type.LinkAja: + compulsoryFields = [ + 'externalID', + 'phone', + 'amount', + 'items', + 'callbackURL', + 'redirectURL', + 'ewalletType', + ]; + break; + default: + reject({ + status: 400, + code: errors.API_VALIDATION_ERROR, + message: 'Invalid EWallet Type', + }); + } + } + + Validate.rejectOnMissingFields(compulsoryFields, data, reject); + + fetchWithHTTPErr(this.API_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: Auth.basicAuthHeader(this.opts.secretKey), + }, + body: JSON.stringify({ + external_id: data.externalID, + amount: data.amount, + phone: data.phone, + expiration_date: data.expirationDate, + callback_url: data.callbackURL, + redirect_url: data.redirectURL, + items: data.items, + ewallet_type: data.ewalletType, + }), + }) + .then(resolve) + .catch(reject); + }); +}; + +EWallet.prototype.getPayment = function(data) { + return promWithJsErr((resolve, reject) => { + Validate.rejectOnMissingFields(['externalID', 'ewalletType'], data, reject); + + const queryStr = data + ? queryStringWithoutUndefined({ + external_id: data.externalID, + ewallet_type: data.ewalletType, + }) + : ''; + + fetchWithHTTPErr(`${this.API_ENDPOINT}?${queryStr}`, { + method: 'GET', + headers: { + Authorization: Auth.basicAuthHeader(this.opts.secretKey), + }, + }) + .then(resolve) + .catch(reject); + }); +}; + +module.exports = EWallet; diff --git a/src/ewallet/index.js b/src/ewallet/index.js new file mode 100644 index 0000000..6636228 --- /dev/null +++ b/src/ewallet/index.js @@ -0,0 +1,3 @@ +const EWalletService = require('./ewallet'); + +module.exports = { EWalletService }; diff --git a/src/xendit.d.ts b/src/xendit.d.ts index 0db07fc..a908588 100644 --- a/src/xendit.d.ts +++ b/src/xendit.d.ts @@ -6,6 +6,7 @@ import { InvoiceService } from './invoice'; import { PayoutService } from './payout'; import { RecurringPayment } from './recurring'; import { XenditOptions } from './xendit_opts'; +import { EWalletService } from './ewallet'; export = class Xendit { constructor(opts: XenditOptions); @@ -16,4 +17,5 @@ export = class Xendit { Invoice: typeof InvoiceService; Payout: typeof PayoutService; RecurringPayment: typeof RecurringPayment; + EWallet: typeof EWalletService; }; diff --git a/src/xendit.js b/src/xendit.js index 23fd189..176a644 100644 --- a/src/xendit.js +++ b/src/xendit.js @@ -4,6 +4,7 @@ const { DisbursementService } = require('./disbursement'); const { InvoiceService } = require('./invoice'); const { PayoutService } = require('./payout'); const { RecurringPayment } = require('./recurring'); +const { EWalletService } = require('./ewallet'); const Errors = require('./errors'); function Xendit(options) { @@ -27,6 +28,7 @@ function Xendit(options) { this.RecurringPayment = RecurringPayment._constructorWithInjectedXenditOpts( this.opts, ); + this.EWallet = EWalletService._constructorWithInjectedXenditOpts(this.opts); } Xendit.Errors = Errors; diff --git a/test/ewallet/constants.js b/test/ewallet/constants.js new file mode 100644 index 0000000..a538696 --- /dev/null +++ b/test/ewallet/constants.js @@ -0,0 +1,54 @@ +const OVO_EWALLET_TYPE = 'OVO'; +const DANA_EWALLET_TYPE = 'DANA'; +const LINKAJA_EWALLET_TYPE = 'LINKAJA'; + +const EXT_ID = '123'; +const PHONE = '081234567890'; +const AMOUNT = 10000; +const CALLBACK_URL = 'https://yourwebsite.com/callback'; +const REDIRECT_URL = 'https://yourwebsite.com/order/123'; +const ITEMS = [ + { + id: '123123', + name: 'Phone Case', + price: 100000, + quantity: 1, + }, + { + id: '345678', + name: 'Powerbank', + price: 200000, + quantity: 1, + }, +]; + +const VALID_CREATE_OVO_RESPONSE = { + transaction_date: String(new Date()), + amount: AMOUNT, + external_id: EXT_ID, + ewallet_type: OVO_EWALLET_TYPE, + business_id: '12121212', +}; + +const VALID_GET_OVO_PAYMENT_STATUS_RESPONSE = { + external_id: EXT_ID, + amount: AMOUNT, + transaction_date: String(new Date()), + business_id: '12121212', + ewallet_type: OVO_EWALLET_TYPE, + status: 'COMPLETED', +}; + +module.exports = { + OVO_EWALLET_TYPE, + DANA_EWALLET_TYPE, + LINKAJA_EWALLET_TYPE, + EXT_ID, + PHONE, + AMOUNT, + CALLBACK_URL, + REDIRECT_URL, + ITEMS, + VALID_CREATE_OVO_RESPONSE, + VALID_GET_OVO_PAYMENT_STATUS_RESPONSE, +}; diff --git a/test/ewallet/ewallet.test.js b/test/ewallet/ewallet.test.js new file mode 100644 index 0000000..2d2dc28 --- /dev/null +++ b/test/ewallet/ewallet.test.js @@ -0,0 +1,145 @@ +const chai = require('chai'); +const chaiAsProm = require('chai-as-promised'); +const TestConstants = require('./constants'); +const { expect } = chai; +const nock = require('nock'); +const { Errors } = require('../../src/xendit'); +const Xendit = require('../../src/xendit'); + +const x = new Xendit({ + publicKey: 'fake_public_key', + secretKey: 'fake_secret_key', +}); + +chai.use(chaiAsProm); + +const { EWallet } = x; +let ewallet; +beforeEach(function() { + ewallet = new EWallet({}); +}); +before(function() { + nock(x.opts.xenditURL) + .post('/ewallets', { + external_id: TestConstants.EXT_ID, + phone: TestConstants.PHONE, + amount: TestConstants.AMOUNT, + ewallet_type: TestConstants.OVO_EWALLET_TYPE, + }) + .reply(200, TestConstants.VALID_CREATE_OVO_RESPONSE); + nock(x.opts.xenditURL) + .get( + `/ewallets?external_id=${TestConstants.EXT_ID}&ewallet_type=${TestConstants.OVO_EWALLET_TYPE}`, + ) + .reply(200, TestConstants.VALID_GET_OVO_PAYMENT_STATUS_RESPONSE); +}); + +describe('EWallet Service', function() { + describe('createPayment', () => { + it('should create an OVO Payment', done => { + expect( + ewallet.createPayment({ + externalID: TestConstants.EXT_ID, + phone: TestConstants.PHONE, + amount: TestConstants.AMOUNT, + ewalletType: TestConstants.OVO_EWALLET_TYPE, + }), + ) + .to.eventually.deep.equal(TestConstants.VALID_CREATE_OVO_RESPONSE) + .then(() => done()) + .catch(e => done(e)); + }); + it('should report missing required fields', done => { + expect(ewallet.createPayment({})) + .to.eventually.to.be.rejected.then(e => + Promise.all([ + expect(e).to.have.property('status', 400), + expect(e).to.have.property('code', Errors.API_VALIDATION_ERROR), + ]), + ) + .then(() => done()) + .catch(e => done(e)); + }); + it('should report missing OVO required fields', done => { + expect( + ewallet.createPayment({ + external_id: TestConstants.EXT_ID, + amount: TestConstants.AMOUNT, + ewallet_type: TestConstants.OVO_EWALLET_TYPE, + }), + ) + .to.eventually.to.be.rejected.then(e => + Promise.all([ + expect(e).to.have.property('status', 400), + expect(e).to.have.property('code', Errors.API_VALIDATION_ERROR), + ]), + ) + .then(() => done()) + .catch(e => done(e)); + }); + it('should report missing Dana required fields', done => { + expect( + ewallet.createPayment({ + externalID: TestConstants.EXT_ID, + amount: TestConstants.AMOUNT, + redirectURL: TestConstants.REDIRECT_URL, + ewallet_type: TestConstants.DANA_EWALLET_TYPE, + }), + ) + .to.eventually.to.be.rejected.then(e => + Promise.all([ + expect(e).to.have.property('status', 400), + expect(e).to.have.property('code', Errors.API_VALIDATION_ERROR), + ]), + ) + .then(() => done()) + .catch(e => done(e)); + }); + it('should report missing LinkAja required fields', done => { + expect( + ewallet.createPayment({ + externalID: TestConstants.EXT_ID, + phone: TestConstants.PHONE, + amount: TestConstants.AMOUNT, + callbackURL: TestConstants.CALLBACK_URL, + redirectURL: TestConstants.REDIRECT_URL, + }), + ) + .to.eventually.to.be.rejected.then(e => + Promise.all([ + expect(e).to.have.property('status', 400), + expect(e).to.have.property('code', Errors.API_VALIDATION_ERROR), + ]), + ) + .then(() => done()) + .catch(e => done(e)); + }); + }); + + describe('getPayment', () => { + it('should get OVO Payment Status', done => { + expect( + ewallet.getPayment({ + externalID: TestConstants.EXT_ID, + ewalletType: TestConstants.OVO_EWALLET_TYPE, + }), + ) + .to.eventually.deep.equal( + TestConstants.VALID_GET_OVO_PAYMENT_STATUS_RESPONSE, + ) + .then(() => done()) + .catch(e => done(e)); + }); + it('should report missing required fields', done => { + expect(ewallet.getPayment({})) + .to.eventually.to.be.rejected.then(e => + Promise.all([ + expect(e).to.have.property('status', 400), + expect(e).to.have.property('code', Errors.API_VALIDATION_ERROR), + ]), + ) + .then(() => done()) + .catch(e => done(e)); + }); + }); +});