From a0bf9344fdeb7bca62f6526c45cd3120d92883db Mon Sep 17 00:00:00 2001 From: Dharnit <85389770+dharmasatrya@users.noreply.github.com> Date: Mon, 7 Nov 2022 14:34:51 +0700 Subject: [PATCH 1/4] APPL-1039/Unified-Refund --- examples/with_async/refund.js | 28 +++++++ examples/with_promises/refund.js | 34 ++++++++ integration_test/index.js | 1 + integration_test/refund.test.js | 28 +++++++ src/refund/index.d.ts | 3 + src/refund/index.js | 3 + src/refund/refund.d.ts | 39 +++++++++ src/refund/refund.js | 137 +++++++++++++++++++++++++++++++ src/xendit.d.ts | 2 + src/xendit.js | 2 + test/refund/constans.js | 72 ++++++++++++++++ test/refund/refund.test.js | 66 +++++++++++++++ 12 files changed, 415 insertions(+) create mode 100644 examples/with_async/refund.js create mode 100644 examples/with_promises/refund.js create mode 100644 integration_test/refund.test.js create mode 100644 src/refund/index.d.ts create mode 100644 src/refund/index.js create mode 100644 src/refund/refund.d.ts create mode 100644 src/refund/refund.js create mode 100644 test/refund/constans.js create mode 100644 test/refund/refund.test.js diff --git a/examples/with_async/refund.js b/examples/with_async/refund.js new file mode 100644 index 0000000..342cd98 --- /dev/null +++ b/examples/with_async/refund.js @@ -0,0 +1,28 @@ +const x = require('../xendit'); + +const { Refund } = x; +const r = new Refund({}); + +(async function() { + try { + let refund = await r.createRefund({ + invoice_id: '63676ed0eb10cf38ce0550b7', + reason: 'OTHERS', + amount: 1, + }); + console.log('created refund', refund); // eslint-disable-line no-console + + const refundDetails = await r.getRefundById({ id: refund.id }); + // eslint-disable-next-line no-console + console.log('retrieved refund', refundDetails); + + const refundList = await r.listRefunds({}); + // eslint-disable-next-line no-console + console.log('list of refunds', refundList); + + process.exit(0); + } catch (e) { + console.error(e); // eslint-disable-line no-console + process.exit(1); + } +})(); diff --git a/examples/with_promises/refund.js b/examples/with_promises/refund.js new file mode 100644 index 0000000..257f451 --- /dev/null +++ b/examples/with_promises/refund.js @@ -0,0 +1,34 @@ +const x = require('../xendit'); + +const Refund = x.Refund; +const ref = new Refund(); + +ref + .createRefund({ + invoice_id: '63676ed0eb10cf38ce0550b7', + reason: 'OTHERS', + amount: 1, + }) + .then(r => { + // eslint-disable-next-line no-console + console.log('refund created:', r); + return r; + }) + .then(({ id }) => ref.getRefundById({ id })) + .then(r => { + // eslint-disable-next-line no-console + console.log('refund details:', r); + return r; + }) + .then(() => { + return ref.listRefunds({}); + }) + .then(r => { + // eslint-disable-next-line no-console + console.log(':', r); + return r; + }) + .catch(e => { + console.error(e); // eslint-disable-line no-console + process.exit(1); + }); diff --git a/integration_test/index.js b/integration_test/index.js index 7afb52f..866bb75 100644 --- a/integration_test/index.js +++ b/integration_test/index.js @@ -15,6 +15,7 @@ Promise.all([ require('./direct_debit.test')(), require('./report.test')(), require('./transaction.test')(), + // require('./refund.test')() //test disabled until refunds endpoint is fixed ]) .then(() => { Promise.all([require('./regional_retail_outlet.test')()]).then(() => diff --git a/integration_test/refund.test.js b/integration_test/refund.test.js new file mode 100644 index 0000000..8c1d187 --- /dev/null +++ b/integration_test/refund.test.js @@ -0,0 +1,28 @@ +const x = require('./xendit.test'); + +const { Refund } = x; +const r = new Refund({}); + +module.exports = function() { + return r + .createRefund({ + invoice_id: '63676ed0eb10cf38ce0550b7', + reason: 'FRAUDULENT', + amount: 1, + }) + .then(id => { + r.getRefundById({ id }); + }) + .then(() => { + r.listRefunds({}); + }) + .then(() => { + // eslint-disable-next-line no-console + console.log('QR Code integration test done...'); + }) + .catch(e => { + throw new Error( + `Recurring integration tests failed with error: ${e.message}`, + ); + }); +}; diff --git a/src/refund/index.d.ts b/src/refund/index.d.ts new file mode 100644 index 0000000..99fbb45 --- /dev/null +++ b/src/refund/index.d.ts @@ -0,0 +1,3 @@ +import RefundService from './refund'; + +export { RefundService }; diff --git a/src/refund/index.js b/src/refund/index.js new file mode 100644 index 0000000..abbc369 --- /dev/null +++ b/src/refund/index.js @@ -0,0 +1,3 @@ +const RefundService = require('./refund'); + +module.exports = { RefundService }; diff --git a/src/refund/refund.d.ts b/src/refund/refund.d.ts new file mode 100644 index 0000000..5f9a4b6 --- /dev/null +++ b/src/refund/refund.d.ts @@ -0,0 +1,39 @@ +enum RefundReasons { + Fraudulent = 'FRAUDULENT', + Duplicate = 'DUPLICATE', + RequestedByCustomer = 'REQUESTED_BY_CUSTOMER', + Cancellation = 'CANCELLATION', + Others = 'OTHERS', +} + +export = class Refund { + constructor({}); + static _constructorWithInjectedXenditOpts: ( + opts: XenditOptions, + ) => typeof Refund; + + createRefund(data: { + payment_request_id?: string; + reference_id?: string; + invoice_id?: string; + currency?: string; + amount?: number; + reason: RefundReasons; + metadata?: object; + idempotencty_key?: string; + for_user_id?: string; + }): Promise; + + listRefunds(data: { + payment_request_id?: string; + invoice_id?: string; + payment_method_type?: string; + channel_code?: string; + limit?: number; + after_id?: string; + before_id?: string; + for_user_id?: string; + }): Promise; + + getRefundById(data: { id: string }): Promise; +}; diff --git a/src/refund/refund.js b/src/refund/refund.js new file mode 100644 index 0000000..628b771 --- /dev/null +++ b/src/refund/refund.js @@ -0,0 +1,137 @@ +const { + promWithJsErr, + Validate, + fetchWithHTTPErr, + Auth, + queryStringWithoutUndefined, +} = require('../utils'); + +const REFUND_PATH = '/refunds'; + +function Refund(options) { + let aggOpts = options; + if (Refund._injectedOpts && Object.keys(Refund._injectedOpts).length > 0) { + aggOpts = Object.assign({}, options, Refund._injectedOpts); + } + + this.opts = aggOpts; + this.API_ENDPOINT = this.opts.xenditURL + REFUND_PATH; +} + +Refund._injectedOpts = {}; +Refund._constructorWithInjectedXenditOpts = function(options) { + Refund._injectedOpts = options; + return Refund; +}; + +Refund.prototype.createRefund = function(data) { + return promWithJsErr((resolve, reject) => { + Validate.rejectOnMissingFields(['reason', 'amount'], data, reject); + + let headers = { + Authorization: Auth.basicAuthHeader(this.opts.secretKey), + 'Content-Type': 'application/json', + }; + + if (data && data.for_user_id) { + headers['for-user-id'] = data.for_user_id; + } + + if (data && data.idempotency_key) { + headers['idempotency-key'] = data.idempotency_key; + } + + fetchWithHTTPErr(`${this.API_ENDPOINT}`, { + method: 'POST', + headers, + body: JSON.stringify({ + payment_request_id: data.payment_request_id, + reference_id: data.reference_id, + invoice_id: data.invoice_id, + currency: data.currency, + amount: data.amount, + reason: data.reason, + metadata: data.metadata, + }), + }) + .then(resolve) + .catch(reject); + }); +}; + +Refund.prototype.listRefunds = function(data) { + return promWithJsErr((resolve, reject) => { + Validate.rejectOnMissingFields([], data, reject); + + let headers = { + Authorization: Auth.basicAuthHeader(this.opts.secretKey), + 'Content-Type': 'application/json', + }; + + if (data && data.for_user_id) { + headers['for-user-id'] = data.forUserID; + } + + if (data && data.idempotency_key) { + headers['idempotency-key'] = data.idempotency_key; + } + + fetchWithHTTPErr(`${this.API_ENDPOINT}/${data.id}`, { + method: 'GET', + headers, + body: JSON.stringify({ + payment_request_id: data.payment_request_id, + invoice_id: data.invoice_id, + payment_method_type: data.payment_method_type, + channel_code: data.channel_code, + limit: data.limit, + after_id: data.after_id, + before_id: data.before_id, + }), + }) + .then(resolve) + .catch(reject); + }); +}; + +Refund.prototype.getRefundById = function(data) { + return promWithJsErr((resolve, reject) => { + Validate.rejectOnMissingFields([], data, reject); + + let headers = { + Authorization: Auth.basicAuthHeader(this.opts.secretKey), + 'Content-Type': 'application/json', + }; + + if (data && data.for_user_id) { + headers['for-user-id'] = data.for_user_id; + } + + const queryStr = data + ? queryStringWithoutUndefined({ + payment_request_id: data.payment_request_id + ? data.payment_request_id + : undefined, + invoice_id: data.invoice_id ? data.invoice_id : undefined, + payment_method_id: data.payment_method_id + ? data.payment_method_id + : undefined, + channel_code: data.channel_code ? data.channel_code : undefined, + limit: data.limit ? data.limit : undefined, + after_id: data.after_id ? data.after_id : undefined, + before_id: data.before_id ? data.before_id : undefined, + }) + : ''; + + const queryStrWithQuestionMark = queryStr ? `?${queryStr}` : ''; + + fetchWithHTTPErr(`${this.API_ENDPOINT}${queryStrWithQuestionMark}`, { + method: 'GET', + headers, + }) + .then(resolve) + .catch(reject); + }); +}; + +module.exports = Refund; diff --git a/src/xendit.d.ts b/src/xendit.d.ts index 3e620a1..8131e33 100644 --- a/src/xendit.d.ts +++ b/src/xendit.d.ts @@ -16,6 +16,7 @@ import { CustomerService } from './customer'; import { DirectDebitService } from './direct_debit'; import { ReportService } from './report'; import { TransactionService } from './transaction'; +import { RefundService } from './refund'; declare class Xendit { constructor(opts: XenditOptions); @@ -37,5 +38,6 @@ declare class Xendit { DirectDebit: typeof DirectDebitService; Report: typeof ReportService; Transaction: typeof TransactionService; + Refund: typeof RefundService; } export = Xendit; diff --git a/src/xendit.js b/src/xendit.js index b53e627..569a36c 100644 --- a/src/xendit.js +++ b/src/xendit.js @@ -15,6 +15,7 @@ const { DirectDebitService } = require('./direct_debit'); const { RegionalRetailOutletService } = require('./regional_retail_outlet'); const { ReportService } = require('./report'); const { TransactionService } = require('./transaction'); +const { RefundService } = require('./refund'); const Errors = require('./errors'); function Xendit(options) { @@ -57,6 +58,7 @@ function Xendit(options) { this.Transaction = TransactionService._constructorWithInjectedXenditOpts( this.opts, ); + this.Refund = RefundService._constructorWithInjectedXenditOpts(this.opts); } Xendit.Errors = Errors; diff --git a/test/refund/constans.js b/test/refund/constans.js new file mode 100644 index 0000000..895a6c9 --- /dev/null +++ b/test/refund/constans.js @@ -0,0 +1,72 @@ +const CREATE_REFUND_SUCCESS_RESPONSE = { + id: 'rfd-6f4a377d-a201-437f-9119-f8b00cbbe857', + payment_id: 'ddpy-3cd658ae-25b9-4659-aa36-596ae41a809f', + invoice_id: null, + amount: 10000, + payment_method_type: 'DIRECT_DEBIT', + channel_code: 'BPI', + currency: 'PHP', + status: 'SUCCEEDED', + reason: 'CANCELLATION', + reference_id: 'b2756a1e-e6cd-4352-9a68-0483aa2b6a2', + failure_code: null, + refund_fee_amount: null, + created: '2020-08-30T09:12:33.001Z', + updated: '2020-08-30T09:12:33.001Z', + metadata: null, +}; + +const LIST_REFUNDS_SUCCESS_RESPONSE = { + data: [ + { + id: 'rfd-6f4a377d-a201-437f-9119-f8b00cbbe857', + payment_id: 'ddpy-3cd658ae-25b9-4659-aa36-596ae41a809f', + invoice_id: null, + amount: 10000, + payment_method_type: 'DIRECT_DEBIT', + channel_code: 'BPI', + currency: 'PHP', + status: 'SUCCEEDED', + reason: 'CANCELLATION', + reference_id: 'b2756a1e-e6cd-4352-9a68-0483aa2b6a2', + failure_code: null, + refund_fee_amount: null, + created: '2020-08-30T09:12:33.001Z', + updated: '2020-08-30T09:12:33.001Z', + metadata: null, + }, + ], + links: [ + { + href: + "/refunds?after_id='rfd-7a836151-7a2c-4cc9-b158-07a617cc0e3a'&limit=10", + rel: 'first', + method: 'GET', + }, + ], + has_more: true, +}; + +const GET_REFUND_BY_ID_RESPONSE = { + id: 'rfd-6f4a377d-a201-437f-9119-f8b00cbbe857', + payment_id: 'ddpy-3cd658ae-25b9-4659-aa36-596ae41a809f', + invoice_id: null, + amount: 10000, + payment_method_type: 'DIRECT_DEBIT', + channel_code: 'BPI', + currency: 'PHP', + status: 'SUCCEEDED', + reason: 'CANCELLATION', + reference_id: 'b2756a1e-e6cd-4352-9a68-0483aa2b6a2', + failure_code: null, + refund_fee_amount: null, + created: '2020-08-30T09:12:33.001Z', + updated: '2020-08-30T09:12:33.001Z', + metadata: null, +}; + +module.exports = { + CREATE_REFUND_SUCCESS_RESPONSE, + LIST_REFUNDS_SUCCESS_RESPONSE, + GET_REFUND_BY_ID_RESPONSE, +}; diff --git a/test/refund/refund.test.js b/test/refund/refund.test.js new file mode 100644 index 0000000..79293ea --- /dev/null +++ b/test/refund/refund.test.js @@ -0,0 +1,66 @@ +const chai = require('chai'); +const chaiAsProm = require('chai-as-promised'); +const { expect } = chai; +const nock = require('nock'); +const Xendit = require('../../src/xendit'); +const { + CREATE_REFUND_SUCCESS_RESPONSE, + LIST_REFUNDS_SUCCESS_RESPONSE, + GET_REFUND_BY_ID_RESPONSE, +} = require('./constants'); + +const x = new Xendit({ + secretKey: 'fake_secret_key', +}); + +chai.use(chaiAsProm); + +const { Refund } = x; +let r = new Refund({}); +before(function() { + nock(x.opts.xenditURL) + .post('/refunds', { + invoice_id: '63676ed0eb10cf38ce0550b7', + reason: 'OTHERS', + amount: 1, + }) + .reply(201, CREATE_REFUND_SUCCESS_RESPONSE) + .get('/refunds') + .reply(200, LIST_REFUNDS_SUCCESS_RESPONSE) + .get('/refunds/rfd-e9601c54-cacc-4b77-90e7-17c899c19106') + .reply(200, GET_REFUND_BY_ID_RESPONSE); +}); + +describe('Refund Service', () => { + describe('create refund', () => { + it('should get a response of refund created', done => { + expect( + r.createRefund({ + invoice_id: '63676ed0eb10cf38ce0550b7', + reason: 'OTHERS', + amount: 1, + }), + ) + .to.eventually.deep.equal(CREATE_REFUND_SUCCESS_RESPONSE) + .and.notify(done); + }); + }); + describe('list refunds', () => { + it('should get a response of refunds', done => { + expect(r.listRefunds({})) + .to.eventually.deep.equal(LIST_REFUNDS_SUCCESS_RESPONSE) + .and.notify(done); + }); + }); + describe('get refund by id', () => { + it('should get a response detail of refund by id', done => { + expect( + r.getRefundById({ + id: 'rfd-e9601c54-cacc-4b77-90e7-17c899c19106', + }), + ) + .to.eventually.deep.equal(GET_REFUND_BY_ID_RESPONSE) + .and.notify(done); + }); + }); +}); From fee3823ed8f0a8bec69c70c708ddff4a5f3f15f6 Mon Sep 17 00:00:00 2001 From: Dharnit <85389770+dharmasatrya@users.noreply.github.com> Date: Mon, 7 Nov 2022 14:36:14 +0700 Subject: [PATCH 2/4] APPL-1039/Unified-Refund --- test/refund/refund.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/refund/refund.test.js b/test/refund/refund.test.js index 79293ea..d81b5e3 100644 --- a/test/refund/refund.test.js +++ b/test/refund/refund.test.js @@ -7,7 +7,7 @@ const { CREATE_REFUND_SUCCESS_RESPONSE, LIST_REFUNDS_SUCCESS_RESPONSE, GET_REFUND_BY_ID_RESPONSE, -} = require('./constants'); +} = require('./constans'); const x = new Xendit({ secretKey: 'fake_secret_key', From 82d38bf4c49529824367eb1025a885ff08a72c09 Mon Sep 17 00:00:00 2001 From: Dharnit <85389770+dharmasatrya@users.noreply.github.com> Date: Mon, 7 Nov 2022 14:47:38 +0700 Subject: [PATCH 3/4] APPL-1039/Unified-Refund --- src/refund/refund.js | 15 +++------------ test/refund/refund.test.js | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/refund/refund.js b/src/refund/refund.js index 628b771..3d08f2e 100644 --- a/src/refund/refund.js +++ b/src/refund/refund.js @@ -59,9 +59,9 @@ Refund.prototype.createRefund = function(data) { }); }; -Refund.prototype.listRefunds = function(data) { +Refund.prototype.getRefundById = function(data) { return promWithJsErr((resolve, reject) => { - Validate.rejectOnMissingFields([], data, reject); + Validate.rejectOnMissingFields(['id'], data, reject); let headers = { Authorization: Auth.basicAuthHeader(this.opts.secretKey), @@ -79,22 +79,13 @@ Refund.prototype.listRefunds = function(data) { fetchWithHTTPErr(`${this.API_ENDPOINT}/${data.id}`, { method: 'GET', headers, - body: JSON.stringify({ - payment_request_id: data.payment_request_id, - invoice_id: data.invoice_id, - payment_method_type: data.payment_method_type, - channel_code: data.channel_code, - limit: data.limit, - after_id: data.after_id, - before_id: data.before_id, - }), }) .then(resolve) .catch(reject); }); }; -Refund.prototype.getRefundById = function(data) { +Refund.prototype.listRefunds = function(data) { return promWithJsErr((resolve, reject) => { Validate.rejectOnMissingFields([], data, reject); diff --git a/test/refund/refund.test.js b/test/refund/refund.test.js index d81b5e3..7b6888a 100644 --- a/test/refund/refund.test.js +++ b/test/refund/refund.test.js @@ -47,7 +47,7 @@ describe('Refund Service', () => { }); describe('list refunds', () => { it('should get a response of refunds', done => { - expect(r.listRefunds({})) + expect(r.listRefunds()) .to.eventually.deep.equal(LIST_REFUNDS_SUCCESS_RESPONSE) .and.notify(done); }); From 5e4dc76877aae71344c20d66b09cc26753ad9a31 Mon Sep 17 00:00:00 2001 From: Dharnit <85389770+dharmasatrya@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:38:58 +0700 Subject: [PATCH 4/4] add readme --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 88d89cc..ddedb92 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ For PCI compliance to be maintained, tokenization of credit cards info should be - [Set Callback URL](#set-callback-url) - [Create transfers](#create-transfers) - [Create fee rules](#create-fee-rules) + - [Refund Services](#refund-services) + - [Create refund](#create-refund-1) + - [List refunds](#list-refunds) + - [Get refund details by ID](#get-refund-details-by-id) - [Contributing](#contributing) @@ -1599,6 +1603,68 @@ p.createFeeRule(data: { }) ``` +### Refund Services + +Instanitiate Refund service using constructor that has been injected with Xendit keys + +```js +const { Refund } = x; +const r = new Refund(); +``` + +Example: Create a refund + +```js +r.createRefund({ + invoice_id: 'your-invoice-id', + reason: 'FRAUDULENT', + amount: 1000, +}).then(({ id }) => { + console.log(`refund created with ID: ${id}`); +}); +``` + +Refer to [Xendit API Reference](https://developers.xendit.co/api-reference/#refunds) for more info about methods' parameters + +#### Create refund + +```ts +r.createRefund(data: { + payment_request_id?: string; + reference_id?: string; + invoice_id?: string; + currency?: string; + amount?: number; + reason: RefundReasons; + metadata?: object; + idempotencty_key?: string; + for_user_id?: string; +}) +``` + +#### List refunds + +```ts +r.listRefunds(data: { + payment_request_id?: string; + invoice_id?: string; + payment_method_type?: string; + channel_code?: string; + limit?: number; + after_id?: string; + before_id?: string; + for_user_id?: string; +}) +``` + +#### Get refund details by ID + +```ts +r.getRefundById(data: { + id: string; +}) +``` + ## Contributing Running test suite