diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a8497..f09f9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.1.8 | 2024-02-19 +This release, we've addressed several Hotfixes and added support for the new NIBBS BVN verification flow. Check out the details below: + +### Version Changes. +- [ADDED] NIBBS BVN verification flow. +- [ADDED] unit tests for the new BVN verification flow, transaction fees, and Card Collection (split payments) into subaccounts. +- [ADDED] 'subaccounts' as an optional body parameter for card charge and PWBT (Pay with Bank Transfer). +- [FIXED] "URL Not Found" Error returned from Validate Bill Service method. +- [FIXED] "Invalid currency provided" Error returned from the Transaction fees method. + ## 1.1.7 | 2024-01-25 In this release, we've enhanced payment methods and addressed various housekeeping issues to ensure a smoother experience. Check out the details below: diff --git a/documentation/misc.md b/documentation/misc.md index d87cb07..60b50f9 100644 --- a/documentation/misc.md +++ b/documentation/misc.md @@ -212,9 +212,9 @@ Sample Response } ``` -## Resolve bvn details +## Initiate BVN Consent -This describes how to fetch bvn information +This describes how to initiate bvn consent flow for your customer. ```javascript const Flutterwave = require('flutterwave-node-v3'); @@ -223,11 +223,14 @@ const flw = new Flutterwave(process.env.FLW_PUBLIC_KEY, process.env.FLW_SECRET_K -const resolveBvn = async () => { +const initiateBvn = async () => { try { const payload = { - "bvn": "123456789010" + "bvn": "12347832211", + "firstname": "Lyra", + "lastname:" "Balacqua", + "redirect_url": "https://example-url.company.com" } const response = await flw.Misc.bvn(payload) console.log(response); @@ -238,35 +241,65 @@ const resolveBvn = async () => { } -resolveBvn(); +initiateBvn(); ``` Sample Response ```javascript { - "status": "success", - "message": "BVN details fetched", - "data": { - "bvn": "123456789", - "first_name": "Wendy", - "middle_name": "Chucky", - "last_name": "Rhoades", - "date_of_birth": "01-01-1905", - "phone_number": "08012345678", - "registration_date": "01-01-1921", - "enrollment_bank": "044", - "enrollment_branch": "Idejo", - "image_base_64": null, - "address": null, - "gender": "Male", - "email": null, - "watch_listed": null, - "nationality": "Nigerian", - "marital_status": null, - "state_of_residence": null, - "lga_of_residence": null, - "image": null + "status":"success", + "message":"Bvn verification initiated", + "data":{ + "url":"https://nibss-bvn-consent-management.dev-flutterwave.com/cms/BvnConsent?session=MWNkNDI4ZWYtMjgwNy00ZjA1LWE5NzUtNzUyZGUyZDRjZWQz", + "reference":"FLW71DC60942BAD76D2BD5B4E" + } +} +``` + +## Verify BVN consent + +This describes how to Verify consent and retirve the customer's BVN information. + +```javascript +const Flutterwave = require('flutterwave-node-v3'); + +const flw = new Flutterwave(process.env.FLW_PUBLIC_KEY, process.env.FLW_SECRET_KEY ); + + + +const verifyBvn = async () => { + + try { + const payload = { + "reference":"FLW71DC60942BAD76D2BD5B4E" + } + const response = await flw.Misc.verifybvn(payload) + console.log(response); + } catch (error) { + console.log(error) + } + +} + + +verifyBVN(); +``` + +Sample Response + +```javascript +{ + "status":"success", + "message":"Bvn details fetched", + "data":{ + "first_name":"Lyra", + "last_name":"Balacqua", + "status":"INITIATED", + "reference":"FLW71DC60942BAD76D2BD5B4E", + "callback_url":null, + "bvn_data":null, + "created_at":"2024-02-16T08:28:10.000Z" } } ``` diff --git a/lib/rave.misc.js b/lib/rave.misc.js index 686a674..e6663c3 100644 --- a/lib/rave.misc.js +++ b/lib/rave.misc.js @@ -1,6 +1,7 @@ const balances_currency = require('../services/misc/rave.balances-currency'); const get_bal = require('../services/misc/rave.balances'); -const bankBVN = require('../services/misc/rave.bvn'); +const initBVN = require('../services/misc/rave.initiate.bvn'); +const verifBVN = require('../services/misc/rave.verify.bvn') const resolve_act = require('../services/misc/rave.resolve.account'); function Misc(RaveBase) { @@ -13,8 +14,13 @@ function Misc(RaveBase) { }; this.bvn = function (data) { - return bankBVN(data, RaveBase); + return initBVN(data, RaveBase); }; + + this.verifybvn = function (data) { + return verifBVN(data, RaveBase); + } + this.verify_Account = function (data) { return resolve_act(data, RaveBase); }; diff --git a/services/bills/rave.validate-bill.js b/services/bills/rave.validate-bill.js index f325d76..05994de 100644 --- a/services/bills/rave.validate-bill.js +++ b/services/bills/rave.validate-bill.js @@ -5,6 +5,7 @@ const { validateSchema } = require('../schema/bill'); async function service(data, _rave) { validator(validateSchema, data); data.method = 'GET'; + data.excludeQuery = true; const { body: response } = await _rave.request( `v3/bill-items/${data.item_code}/validate?code=${data.code}&customer=${data.customer}`, data, diff --git a/services/misc/rave.bvn.js b/services/misc/rave.initiate.bvn.js similarity index 57% rename from services/misc/rave.bvn.js rename to services/misc/rave.initiate.bvn.js index 9d43b0f..66eb656 100644 --- a/services/misc/rave.bvn.js +++ b/services/misc/rave.initiate.bvn.js @@ -1,15 +1,14 @@ const { logger } = require('../../utils/logger'); const { validator } = require('../../utils/validator'); -const { validateBVNSchema } = require('../schema/auxillary'); +const { initiateBVNSchema } = require('../schema/auxillary'); async function service(data, _rave) { - validator(validateBVNSchema, data); - data.method = 'GET'; + validator(initiateBVNSchema, data); const { body: response } = await _rave.request( - `v3/kyc/bvns/${data.bvn}`, + `v3/bvn/verifications`, data, ); - logger(`Resolve BVN details`, _rave); + logger(`Initiate BVN consent`, _rave); return response; } diff --git a/services/misc/rave.verify.bvn.js b/services/misc/rave.verify.bvn.js new file mode 100644 index 0000000..990b4b3 --- /dev/null +++ b/services/misc/rave.verify.bvn.js @@ -0,0 +1,17 @@ +const { logger } = require('../../utils/logger'); +const { validator } = require('../../utils/validator'); +const { verifyBVNSchema } = require('../schema/auxillary'); + +async function service(data, _rave) { + validator(verifyBVNSchema, data); + data.method = 'GET'; + data.excludeQuery = true; + const { body: response } = await _rave.request( + `v3/bvn/verifications/${data.reference}`, + data, + ); + logger(`Verify BVN consent`, _rave); + return response; +} + +module.exports = service; \ No newline at end of file diff --git a/services/schema/auxillary.js b/services/schema/auxillary.js index 587b59f..d4b4551 100644 --- a/services/schema/auxillary.js +++ b/services/schema/auxillary.js @@ -139,8 +139,15 @@ const validateSchema = joi.object({ }); // validate a BVN -const validateBVNSchema = joi.object({ +const initiateBVNSchema = joi.object({ bvn: joi.string().length(11).required(), + firstname: joi.string().max(100).required(), + lastname: joi.string().max(100).required(), + redirect_url: joi.string().uri(), +}); + +const verifyBVNSchema = joi.object({ + reference: joi.string().trim().max(100).required(), }); module.exports = { @@ -158,5 +165,6 @@ module.exports = { updateTokenSchema, withdrawalSchema, validateSchema, - validateBVNSchema, + initiateBVNSchema, + verifyBVNSchema }; diff --git a/services/schema/create.js b/services/schema/create.js index 890f35f..8f79b99 100644 --- a/services/schema/create.js +++ b/services/schema/create.js @@ -249,6 +249,14 @@ const cardChargeSchema = joi.object({ }), payment_plan: joi.string(), meta: joi.object().pattern(/^[a-zA-Z0-9_]*$/, joi.any()), + subaccounts: joi.array().items( + joi.object({ + id: joi.string().trim().max(100).required(), + transaction_split_ratio: joi.number().positive(), + transaction_charge_type: joi.string().valid('flat', 'percentage', 'flat_subaccount'), + transaction_charge: joi.number().positive(), + }) + ).min(1), }); // initiate collections for different payment methods @@ -282,6 +290,14 @@ const chargeSchema = joi.object({ billing_zip: joi.string(), meta: joi.object().pattern(/^[a-zA-Z0-9_]*$/, joi.any()), expires: joi.number().positive().max(31536000), + subaccounts: joi.array().items( + joi.object({ + id: joi.string().trim().max(100).required(), + transaction_split_ratio: joi.number().positive(), + transaction_charge_type: joi.string().valid('flat', 'percentage', 'flat_subaccount'), + transaction_charge: joi.number().positive(), + }) + ).min(1), }); // create eNaira charge diff --git a/services/transactions/rave.fee.js b/services/transactions/rave.fee.js index 8df8d02..d392d30 100644 --- a/services/transactions/rave.fee.js +++ b/services/transactions/rave.fee.js @@ -5,6 +5,7 @@ const { feeSchema } = require('../schema/auxillary'); async function service(data, _rave) { validator(feeSchema, data); data.method = 'GET'; + data.excludeQuery = true; const { body: response } = await _rave.request( `v3/transactions/fee?amount=${data.amount}¤cy=${data.currency}`, data, diff --git a/test/rave.charge.test.js b/test/rave.charge.test.js index 0418753..2a9577a 100644 --- a/test/rave.charge.test.js +++ b/test/rave.charge.test.js @@ -738,4 +738,92 @@ describe('#Rave charge', function () { expect(err.message).to.include('"currency" is required'); } }); + + it('should return charge into collection subaccounts', async function () { + this.timeout(10000); + + const createCardChargeStub = sinon.stub(chargeInstance, 'card').resolves({ + status: 'success', + message: 'Successful', + data: { + id: 4918672, + tx_ref: 'MC-3243e000', + flw_ref: 'FLW-MOCK-365702bdb12af7938bdd02860caf2bc2', + device_fingerprint: 'N/A', + amount: 100, + charged_amount: 100, + app_fee: 1.4, + merchant_fee: 0, + processor_response: 'Please enter the OTP sent to your mobile number 080****** and email te**@rave**.com', + auth_model: 'NOAUTH', + currency: 'NGN', + ip: '54.75.161.64', + narration: 'CARD Transaction ', + status: 'successful', + auth_url: 'https://ravesandboxapi.flutterwave.com/mockvbvpage?ref=FLW-MOCK-365702bdb12af7938bdd02860caf2bc2&code=00&message=Approved.%20Successful&receiptno=RN1708329200239', + payment_type: 'card', + plan: null, + fraud_status: 'ok', + charge_type: 'normal', + created_at: '2024-02-19T07:53:20.000Z', + account_id: 20937, + customer: { + id: 2356420, + phone_number: null, + name: 'Yolande AglaĆ©', + email: 'user@example.com', + created_at: '2024-02-19T07:53:20.000Z' + }, + card: { + first_6digits: '553188', + last_4digits: '2950', + issuer: 'MASTERCARD CREDIT', + country: 'NG', + type: 'MASTERCARD', + expiry: '09/32' + } + } + }) + + var payload = { + card_number:"5531886652142950", + cvv:"564", + expiry_month:"09", + expiry_year:"32", + currency:"NGN", + amount:"100", + fullname:"Yolande AglaĆ© Colbert", + email:"user@example.com", + tx_ref:"MC-3243e000", + redirect_url:"https://www,flutterwave.ng", + enckey: process.env.ENCRYPTION_KEY, + subaccounts: [ + { + id: "RS_93667D2B73110DFFEF8449A8A0A32415", + transaction_split_ratio: 2, + transaction_charge_type: "flat", + transaction_charge: 100, + }, + { + id: "RS_47CC41E35953182AC35E952D4F4CA713", + transaction_split_ratio: 3, + transaction_charge_type: "flat", + transaction_charge: 100, + }, + { + id: "RS_EEF0D016C26BBF1543F09CEF6090AB49", + transaction_split_ratio: 5, + transaction_charge_type: "flat", + transaction_charge: 100, + }, + ], + }; + var resp = await chargeInstance.card(payload); + expect(createCardChargeStub).to.have.been.calledOnce; + expect(createCardChargeStub).to.have.been.calledOnceWith(payload); + + expect(resp).to.have.property('status', 'success'); + + expect(resp.data).to.have.property('status', 'successful'); + }); }); diff --git a/test/rave.misc.test.js b/test/rave.misc.test.js index 3387479..31ac367 100644 --- a/test/rave.misc.test.js +++ b/test/rave.misc.test.js @@ -164,53 +164,71 @@ describe('#Rave Misc', function () { expect(resp.body.data[0]).to.have.property('ledger_balance'); }); - it('should verify BVN and return success message', async function () { + it('should initiate BVN consent and return success message', async function () { this.timeout(10000); - const resolveBVNSuccessStub = sinon.stub(miscInstance, 'bvn').resolves({ + const resolveInitBVNSuccessStub = sinon.stub(miscInstance, 'bvn').resolves({ body: { status: 'success', - message: 'BVN details fetched', + message: 'Bvn verification initiated', data: { - bvn: '123456789', - first_name: 'Wendy', - middle_name: 'Chucky', - last_name: 'Rhoades', - date_of_birth: '01-01-1905', - phone_number: '08012345678', - registration_date: '01-01-1921', - enrollment_bank: '044', - enrollment_branch: 'Idejo', - image_base_64: null, - address: null, - gender: 'Male', - email: null, - watch_listed: null, - nationality: 'Nigerian', - marital_status: null, - state_of_residence: null, - lga_of_residence: null, - image: null, - }, + url: 'https://nibss-bvn-consent-management.dev-flutterwave.com/cms/BvnConsent?session=MWNkNDI4ZWYtMjgwNy00ZjA1LWE5NzUtNzUyZGUyZDRjZWQz', + reference: 'FLW71DC60942BAD76D2BD5B4E' + } }, }); var payload = { - bvn: '12345678901', + bvn: "12347832211", + firstname: "Lyra", + lastname: "Balacqua", + redirect_url: "https://example-url.company.com" }; var resp = await miscInstance.bvn(payload); - expect(resolveBVNSuccessStub).to.have.been.calledOnce; - expect(resolveBVNSuccessStub).to.have.been.calledOnceWith(payload); + expect(resolveInitBVNSuccessStub).to.have.been.calledOnce; + expect(resolveInitBVNSuccessStub).to.have.been.calledOnceWith(payload); expect(resp.body).to.have.property('status', 'success'); + expect(resp.body).to.have.property('message', 'Bvn verification initiated'); expect(resp.body).to.have.property('data'); - expect(resp.body.data).to.have.property('bvn'); - expect(resp.body.data).to.have.property('first_name'); - expect(resp.body.data).to.have.property('date_of_birth'); - expect(resp.body.data).to.have.property('phone_number'); + expect(resp.body.data).to.have.property('reference'); + expect(resp.body.data).to.have.property('url'); + }); + + it('should verify BVN consent and return success message', async function () { + this.timeout(10000); + + const resolveVerifyBVNSuccessStub = sinon.stub(miscInstance, 'verifybvn').resolves({ + body: { + status: 'success', + message: 'Bvn details fetched', + data: { + first_name: 'Lyra', + last_name: 'Balacqua', + status: 'INITIATED', + reference: 'FLW71DC60942BAD76D2BD5B4E', + callback_url: null, + bvn_data: null, + created_at: '2024-02-16T08:28:10.000Z' + } + }, + }); + + var payload = { + reference: "FLW71DC60942BAD76D2BD5B4E" + }; + + var resp = await miscInstance.verifybvn(payload); + + expect(resolveVerifyBVNSuccessStub).to.have.been.calledOnce; + expect(resolveVerifyBVNSuccessStub).to.have.been.calledOnceWith(payload); + + expect(resp.body).to.have.property('status', 'success'); + expect(resp.body).to.have.property('message', 'Bvn details fetched'); + expect(resp.body).to.have.property('data'); }); it('should verify resolve bank account details', async function () { diff --git a/test/rave.transactions.test.js b/test/rave.transactions.test.js index 543877d..67e844e 100644 --- a/test/rave.transactions.test.js +++ b/test/rave.transactions.test.js @@ -148,4 +148,38 @@ describe('#Rave Transactions', function () { expect(resp.data[0]).to.have.property('note'); expect(resp.data[0]).to.have.property('actor'); }); + + it('should successfully return transaction fee', async function () { + this.timeout(10000); + + const getTransactionFeeStub = sinon + .stub(trxInstance, 'fee') + .resolves({ + status: 'success', + message: 'Charged fee', + data: { + charge_amount: 1000, + fee: 14, + merchant_fee: 0, + flutterwave_fee: 14, + stamp_duty_fee: 0, + currency: 'NGN' + } + }) + + var payload = { + amount: 1000, + currency: "NGN" + }; + + var resp = await trxInstance.fee(payload); + expect(getTransactionFeeStub).to.have.been.calledOnce; + + expect(resp).to.have.property('status', 'success'); + expect(resp).to.have.property('data'); + expect(resp).to.have.property('message', 'Charged fee'); + + expect(resp.data).to.have.property('charge_amount'); + expect(resp.data).to.have.property('fee'); + }); });