├── .editorconfig ├── .github └── pull_request_template.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src ├── PayStack │ ├── extension │ │ ├── Mockable.js │ │ └── facade │ │ │ └── MockFactory.js │ └── index.js └── endpoints │ ├── balance_history.js │ ├── bulk_charges.js │ ├── charges.js │ ├── control_panel_for_sessions.js │ ├── customers.js │ ├── dedicated_nuban.js │ ├── disputes.js │ ├── invoices.js │ ├── miscellaneous.js │ ├── pages.js │ ├── plans.js │ ├── products.js │ ├── refunds.js │ ├── settlements.js │ ├── subaccounts.js │ ├── subscriptions.js │ ├── transactions.js │ ├── transfers.js │ ├── transfers_recipients.js │ └── verifications.js └── test ├── .gitkeep └── paystack-object.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Closes # 9 | 10 | ## 📑 Description 11 | 12 | 13 | 17 | 18 | ## ✅ Checks 19 | 20 | - [ ] My pull request adheres to the code style of this project 21 | - [ ] My code requires changes to the documentation 22 | - [ ] I have updated the documentation as required 23 | - [ ] All the tests have passed 24 | 25 | ## ℹ Additional Information 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # package manager files 59 | package-lock.json 60 | yarn.lock 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # next.js build output 66 | .next 67 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .github 3 | node_modules 4 | .DS_Store 5 | npm-debug.log 6 | test 7 | .travis.yml 8 | .editorconfig 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - node 5 | - 8.4.0 6 | - 9.0.0 7 | - 10.0.0 8 | - 11.0.0 9 | - 12.0.0 10 | 11 | sudo: false 12 | 13 | install: 14 | - npm install 15 | 16 | cache: 17 | directories: 18 | - ~/.npm # cache npm's cache 19 | - ~/npm # cache latest npm 20 | 21 | notifications: 22 | email: 23 | - stitchnig@gmail.com 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 0.3.0 (2021-03-23) 3 | 4 | ### Added Features 5 | - Added the **submitAddress()** Endpoint method 6 | - Added the **chargeUssd()** Endpoint method 7 | - Added the **chargeMobileMoney()** Endpoint method 8 | - Added the **resolveBVNPremium()** Endpoint method 9 | - Added the **listProviders()** Endpoint method 10 | 11 | ### Bug Fixes 12 | - Fixed error _"TypeError: function is not a function"_ when mock is enabled 13 | 14 | 15 | # 0.2.6 (2021-02-27) 16 | 17 | ### Features Added 18 | - Updated `lodash` dependency to _v4.17.21_ from _v4.17.19_ to fix ReDoS vulnerability 19 | - Beta: Added ability to mock `PayStack` instance by [@isocroft](https://github.com/isocroft) 20 | - Added _Dedicated Nuban_ API Endpoint methods by [@isocroft](https://github.com/isocroft) 21 | 22 | 23 | # 0.2.5 (2020-07-18) 24 | 25 | ### Bug Fixes 26 | - Fixed outdated endpoints issue raised by [@Tetranyble](https://github.com/Tetranyble) 27 | - Fixed errors on **chargeCard()** Endpoint method 28 | 29 | 30 | # 0.2.4 (2020-02-25) 31 | 32 | ### Bug Fixes 33 | - Fixed outdated endpoints issue raised by [@navicstein](https://github.com/navicstein) 34 | 35 | 36 | # 0.2.3 (2020-02-04) 37 | 38 | ### Bug Fixes 39 | - Fixed errors on **listBanks()** Endpoint method 40 | 41 | ### Feature Added 42 | - Updated _Miscellaneous_ API Endpoint methods 43 | - Updated _Pages_ API Endpoint methods 44 | - Updated _Verifications_ API Endpoint methods 45 | - Added _Disputes_ API Endpoint methods by [@isocroft](https://github.com/isocroft) 46 | 47 | 48 | # 0.2.2 (2019-11-23) 49 | 50 | ### Feature Added 51 | - Added _Transfer Recipients_ API Endpoint methods by [@adekoyejoakinhanmi](https://github.com/adekoyejoakinhanmi) 52 | 53 | 54 | # 0.2.1 (2019-10-13) 55 | 56 | ### Bug Fixes 57 | - Fixed `SyntaxError: Unexpected end of JSON input` by [@HopePraise5](https://github.com/HopePraise5) 58 | 59 | ### Feature Added 60 | - Added _Products_ API Endpoint methods 61 | 62 | 63 | # 0.2.0 (2019-04-14) 64 | 65 | ### Feature Added 66 | - Added 'Control-Cache' header to limit HTTP caching 67 | - Added _Control Panel Payment Sessions_ API Endpoint methods 68 | 69 | 70 | # 0.1.0 (2019-03-14) 71 | 72 | ### Bug Fixes 73 | - None 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this [StitchNG Paystack Library][project], please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 11 | 2. Update the README.md with details of changes to the interface, this includes new environment 12 | variables, exposed ports, useful file locations and container parameters. 13 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 14 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 15 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 16 | do not have permission to do that, you may request the second reviewer to merge it for you. 17 | 18 | ## Code of Conduct 19 | 20 | - If you are contributing to the repo, kindly update the necessary test file in `/test` or add a new one and ensure all tests are passed before sending a PR. 21 | 22 | - All code sent in MUST pass both linting & testing requirements 23 | 24 | - Code Reviews are done in order to ensure that only quality work is put forward and eventually merged 25 | 26 | ### Our Pledge 27 | 28 | In the interest of fostering an open and welcoming environment, we as 29 | contributors and maintainers pledge to making participation in our project and 30 | our community a harassment-free experience for everyone, regardless of age, body 31 | size, disability, ethnicity, gender identity and expression, level of experience, 32 | nationality, personal appearance, race, religion, or sexual identity and 33 | orientation. 34 | 35 | ### Our Standards 36 | 37 | Examples of behavior that contributes to creating a positive environment 38 | include: 39 | 40 | * Using welcoming and inclusive language 41 | * Being respectful of differing viewpoints and experiences 42 | * Gracefully accepting constructive criticism 43 | * Focusing on what is best for the community 44 | * Showing empathy towards other community members 45 | 46 | Examples of unacceptable behavior by participants include: 47 | 48 | * The use of sexualized language or imagery and unwelcome sexual attention or 49 | advances 50 | * Trolling, insulting/derogatory comments, and personal or political attacks 51 | * Public or private harassment 52 | * Publishing others' private information, such as a physical or electronic 53 | address, without explicit permission 54 | * Other conduct which could reasonably be considered inappropriate in a 55 | professional setting 56 | 57 | ### Our Responsibilities 58 | 59 | Project maintainers are responsible for clarifying the standards of acceptable 60 | behavior and are expected to take appropriate and fair corrective action in 61 | response to any instances of unacceptable behavior. 62 | 63 | Project maintainers have the right and responsibility to remove, edit, or 64 | reject comments, commits, code, wiki edits, issues, and other contributions 65 | that are not aligned to this Code of Conduct, or to ban temporarily or 66 | permanently any contributor for other behaviors that they deem inappropriate, 67 | threatening, offensive, or harmful. 68 | 69 | ### Scope 70 | 71 | This Code of Conduct applies both within project spaces and in public spaces 72 | when an individual is representing the project or its community. Examples of 73 | representing a project or community include using an official project e-mail 74 | address, posting via an official social media account, or acting as an appointed 75 | representative at an online or offline event. Representation of a project may be 76 | further defined and clarified by project maintainers. 77 | 78 | ### Enforcement 79 | 80 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 81 | reported by contacting the project maintainer at [this email](mailto:stitchnig@gmail.com). All 82 | complaints will be reviewed and investigated and will result in a response that 83 | is deemed necessary and appropriate to the circumstances. The project team is 84 | obligated to maintain confidentiality with regard to the reporter of an incident. 85 | Further details of specific enforcement policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership. 90 | 91 | ### Attribution 92 | 93 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, 94 | available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct][version] 95 | 96 | [project]: https://github.com/stitchng/paystack 97 | [homepage]: https://contributor-covenant.org 98 | [version]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 StitchNG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paystack 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![Build Status][travis-image]][travis-url] 5 | 6 | A NodeJS Wrapper for [Paystack](https://www.paystack.com) 7 | 8 | ## Overview 9 | This project provides an easy-to-use object-oriented API to access endpoints delineated at https://developers.paystack.co/reference 10 | 11 | ## Getting Started 12 | 13 | >Install from the NPM Registry 14 | 15 | ```bash 16 | 17 | $ npm i --save paystack-node 18 | 19 | ``` 20 | 21 | # Usage 22 | 23 | ```js 24 | 25 | let PayStack = require('paystack-node') 26 | 27 | let APIKEY = 'sk_live_2hWyQ6HW73jS8p1IkXmSWOlE4y9Inhgyd6g5f2R7' 28 | const environment = process.env.NODE_ENV 29 | 30 | const paystack = new PayStack(APIKEY, environment) 31 | 32 | const feesCalculator = new PayStack.Fees(); 33 | const feeCharge = feesCalculator.calculateFor(250000) // 2,500 Naira 34 | 35 | /* 36 | NOTE: All fields/params that require dates should be set to 37 | instances of the `Date()` constructor as they will 38 | eventually be turned into the ISO 8601 format (String) 39 | using `toJSON()` method for date instances/objects 40 | */ 41 | 42 | const promise_0 = paystack.getSettlements({ 43 | from:new Date("2017-02-09"), 44 | to:new Date() 45 | }) 46 | 47 | promise_0.then(function(response){ 48 | var data = response.body.data; 49 | }).catch(function (error){ 50 | // deal with error 51 | }) 52 | 53 | // listBanks 54 | 55 | try { 56 | let { body: { status, message, data } } = await paystack.listBanks({ 57 | currency: 'NGN' 58 | }); 59 | 60 | if(status === false){ 61 | throw new Error(message); 62 | } 63 | }catch(ex){ 64 | console.error(ex.message); 65 | } 66 | 67 | // addPageProduct 68 | 69 | const promise1 = paystack.addPageProduct({ 70 | id: '0826739', 71 | products: [473, 292] 72 | }) 73 | 74 | promise1.then(function(response){ 75 | // Error Handling 76 | if(response.body.status === false){ 77 | console.error(response.body.message); 78 | } 79 | var data = response.body.data; 80 | }).catch(function (error) { 81 | // deal with error 82 | }) 83 | 84 | // getCustomer 85 | 86 | const promise2 = paystack.getCustomer({ 87 | customer_id:'CUS_e24m6SqA6g3Jk889o21' 88 | }) 89 | 90 | promise2.then(function(response){ 91 | var data = response.body 92 | 93 | }).catch(function(error){ 94 | // deal with error 95 | }) 96 | 97 | // createCustomer 98 | 99 | const promise3 = paystack.createCustomer({ 100 | email:'malik.onyemah@gmail.com', 101 | first_name:'Malik', 102 | last_name:'Onyemah', 103 | phone:'+2347135548369' 104 | }) 105 | 106 | promise3.then(function(response){ 107 | return response.body 108 | }).then( body => { 109 | return res.status(200).json({id:body.data.id}) 110 | }) 111 | 112 | // setRiskActionOnCustomer 113 | 114 | const promise4 = paystack.setRiskActionOnCustomer({ 115 | risk_action:'deny', 116 | customer_id:'CUS_e24m6SqA6g3Jk889o21' 117 | }) 118 | 119 | promise4.then(function (response){ 120 | const result = response.body 121 | }).catch(function (error){ 122 | // deal with error 123 | }) 124 | 125 | // createPage 126 | 127 | const promise5 = paystack.createPage({ 128 | name:'DoorPost Pay', 129 | description:'This is payment for every ', 130 | amount:300000, // Amount in kobo 131 | slug:'5nApBwZkvR', 132 | redirect_url:'https://www.localhoster.com/pay/callback', 133 | custom_fields: ['phone_number', 'age'] 134 | }) 135 | 136 | promise5.then(function (response){ 137 | console.log(response.body); 138 | }).catch(function (error){ 139 | // deal with error 140 | }) 141 | 142 | // initializeTransaction 143 | 144 | const promise6 = paystack.initializeTransaction({ 145 | reference: "7PVGX8MEk85tgeEpVDtD", 146 | amount: 500000, // 5,000 Naira (remember you have to pass amount in kobo) 147 | email: "seun045olayinka@gmail.com", 148 | subaccount: "ACCT_8f4s1eq7ml6rlzj" 149 | }) 150 | 151 | promise6.then(function (response){ 152 | console.log(response.body); 153 | }).catch(function (error){ 154 | // deal with error 155 | }) 156 | 157 | // verifyTransaction 158 | 159 | const promise7 = paystack.verifyTransaction({ 160 | reference: "7PVGX8MEk85tgeEpVDtD" 161 | }) 162 | 163 | promise7.then(function (response){ 164 | console.log(response.body); 165 | }).catch(function (error){ 166 | // deal with error 167 | }) 168 | 169 | // listInvoices 170 | 171 | const promise8 = paystack.listInvoices({ 172 | customer: "CUS_je02lbimlqixzax", 173 | status: "pending", 174 | paid: false, 175 | currency: "NGN" 176 | }) 177 | 178 | promise8.then(function (response){ 179 | console.log(response.body); 180 | }).catch(function (error){ 181 | // deal with error 182 | }) 183 | 184 | app.use(async function verifications(req, res, next){ 185 | let responseBVN = await paystack.resolveBVN({ 186 | bvn:req.body.bvn //'22283643840404' 187 | }) 188 | 189 | let responseAcctNum = await paystack.resolveAccountNumber({ 190 | account_number:req.body.acc_num, // '0004644649' 191 | bank_code:req.body.bank_code // '075' 192 | }) 193 | 194 | next() 195 | }) 196 | 197 | ``` 198 | 199 | ### Mocking the Instance (for Unit/Integration Tests) 200 | >Setting up mocks for testing with the paystack instance is now as easy as fliping a switch like so: 201 | 202 | ```js 203 | 204 | let PayStack = require('paystack-node') 205 | 206 | let APIKEY = 'sk_live_2hWyQ6HW73jS8p1IkXmSWOlE4y9Inhgyd6g5f2R7' 207 | const environment = process.env.NODE_ENV 208 | 209 | const paystack = new PayStack(APIKEY, environment) 210 | 211 | // call the real API methods 212 | const { body } = paystack.chargeCard({ 213 | card:{ 214 | number: '5399837841116788', // mastercard 215 | cvv: '324', 216 | expiry_year: '2024', 217 | expiry_month: '08' 218 | }, 219 | email: 'me.biodunch@xyz.ng', 220 | amount: 15600000 // 156,000 Naira in kobo 221 | }) 222 | 223 | // mocking call made on the constructor 224 | // start mocking 225 | PayStack.engageMock() 226 | 227 | // call the mock API methods 228 | const { body } = await paystack.chargeBank({ 229 | bank: { 230 | code: "050", // Eco Bank 231 | account_number: "0000000000" 232 | }, 233 | email: 'me.biodunch@xyz.ng', 234 | amount: 1850000 // 18,500 Naira in kobo 235 | }) 236 | 237 | // replace mocked methods (! don't use arrow functions !) 238 | PayStack.mockMacro( 239 | 'getCustomers', 240 | async function getCustomers (reqPayload = {}) { 241 | // validation for (reqPayload) is already taken care of! 242 | 243 | // @TODO: optionally, connect to a in-memory db (redis) for mocking purposes 244 | 245 | // return mocked response object 246 | return { status: 200, body: { status: "success", data: reqPayload } }; 247 | }) 248 | 249 | const { body } = await paystack.getCustomers({ 250 | customer_id:'CUS_e24m6SqA6g3Jk889o21' 251 | }) 252 | 253 | // stop mocking 254 | // mocking call made on the constructor 255 | PayStack.disengageMock() 256 | ``` 257 | 258 | ## API Resources 259 | 260 | >Each method expects an object literal with both **route parameters** and **request parameters (query / body)**. Please, go through the _src/endpoints_ folder to see the specific items that should make up the object literal for each method 261 | 262 | - customers 263 | - paystack.createCustomer() 264 | - paystack.getCustomer() 265 | - paystack.listCustomer() 266 | - paystack.updateCustomer() 267 | - paystack.deactivateAuthOnCustomer() 268 | - paystack.setRiskActionOnCustomer() 269 | - disputes 270 | - paystack.listDisputes() 271 | - dedicated nuban 272 | - paystack.createDedicatedNuban() 273 | - paystack.listDedicatedNubans() 274 | - paystack.fetchDedicatedNuban() 275 | - paystack.deactivateDedicatedNuban() 276 | - invoices 277 | - paystack.createInvoice() 278 | - paystack.getMetricsForInvoices() 279 | - paystack.sendInvoiceNotification() 280 | - paystack.listInvoice() 281 | - paystack.updateInvoice() 282 | - paystack.verifyInvoice() 283 | - paystack.finalizeInvoiceDraft() 284 | - paystack.archiveInvoice() 285 | - paystack.markInvoiceAsPaid() 286 | - settlements 287 | - paystack.getSettlements() 288 | - payment sessions {control panel} 289 | - paystack.getPaymentSessionTimeout() 290 | - paystack.updatePaymentSessionTimeout() 291 | - pages 292 | - paystack.createPage() 293 | - paystack.listPages() 294 | - paystack.getPage() 295 | - paystack.updatePage() 296 | - paystack.checkSlugAvailability() 297 | - paystack.addPageProduct() 298 | - products 299 | - paystack.createProduct() 300 | - paystack.listProduct() 301 | - paystack.getProduct() 302 | - paystack.updateProduct() 303 | - transactions 304 | - paystack.initializeTransaction() 305 | - paystack.chargeAuthorization() 306 | - paystack.getTransaction() 307 | - paystack.listTransaction() 308 | - paystack.viewTransactionTimeline() 309 | - paystack.transactionTotals() 310 | - paystack.exportTransaction() 311 | - paystack.requestReauthorization() 312 | - paystack.checkAuthorization() 313 | - paystack.verifyTransaction() 314 | - paystack.partialDebit() 315 | - plans 316 | - paystack.createPlan() 317 | - paystack.getPlan() 318 | - paystack.listPlan() 319 | - paystack.updatePlan() 320 | - refunds 321 | - paystack.createRefund() 322 | - paystack.getRefund() 323 | - paystack.listRefund() 324 | - subscriptions 325 | - paystack.createSubscription() 326 | - paystack.disableSubscription() 327 | - paystack.enableSubscription() 328 | - paystack.getSubscription() 329 | - paystack.listSubscription() 330 | - subaccounts 331 | - paystack.createSubaccount() 332 | - paystack.getSubaccount() 333 | - paystack.listSubaccount() 334 | - paystack.updateSubaccount() 335 | - verifications 336 | - paystack.resolveBVN() 337 | - paystack.resolveBVNPremium() 338 | - paystack.matchBVN() 339 | - paystack.resolveAccountNumber() 340 | - paystack.resolveCardBin() 341 | - paystack.resolvePhoneNumber() 342 | - transfers 343 | - paystack.initiateTransfer() 344 | - paystack.listTransfers() 345 | - paystack.fetchTransfer() 346 | - paystack.finalizeTransfer() 347 | - paystack.initiateBulkTransfer() 348 | - transfer_recipients 349 | - paystack.createTransferRecipient() 350 | - paystack.listTransferRecipients() 351 | - paystack.updateTransferRecipient() 352 | - paystack.deleteTransferRecipient() 353 | - charges 354 | - paystack.chargeCard() 355 | - paystack.chargeBank() 356 | - paystack.chargeUssd() 357 | - paystack.chargeMobileMoney() 358 | - paystack.submitPIN() 359 | - paystack.submitOTP() 360 | - paystack.submitPhone() 361 | - paystack.submitBirthday() 362 | - paystack.submitAddress() 363 | - paystack.checkPendingCharge() 364 | - miscellanous 365 | - paystack.listBanks() 366 | - paystack.listProviders() 367 | - paystack.listCountries() 368 | 369 | # License 370 | 371 | MIT 372 | 373 | # Credits 374 | 375 | - [Ifeora Okechukwu](https://twitter.com/isocroft) 376 | - [Ahmad Abdul-Aziz](https://twitter.com/dev_amaz) 377 | 378 | # Contributing 379 | 380 | See the [CONTRIBUTING.md](https://github.com/stitchng/paystack/blob/master/CONTRIBUTING.md) file for info 381 | 382 | [npm-image]: https://img.shields.io/npm/v/paystack-node.svg?style=flat-square 383 | [npm-url]: https://npmjs.org/package/paystack-node 384 | 385 | [travis-image]: https://img.shields.io/travis/stitchng/paystack/master.svg?style=flat-square 386 | [travis-url]: https://travis-ci.org/stitchng/paystack 387 | 388 | ## Support 389 | 390 | **Coolcodes** is a non-profit software foundation (collective) created by **Oparand** - parent company of StitchNG, Synergixe based in Abuja, Nigeria. You'll find an overview of all our work and supported open source projects on our [Facebook Page](https://www.facebook.com/coolcodes/). 391 | 392 | >Follow us on facebook if you can to get the latest open source software/freeware news and infomation. 393 | 394 | Does your business depend on our open projects? Reach out and support us on [Patreon](https://www.patreon.com/coolcodes/). All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. 395 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PayStack = require('./src/PayStack/index.js') 4 | 5 | PayStack.prototype.version = '0.3.0' 6 | 7 | const Fees = function (cap, additionalCharge, percentage, threshold) { 8 | this.additionalCharge = additionalCharge || Fees.INIT_ADDITIONAL_CHARGE 9 | this.percentage = percentage || Fees.INIT_PERCENT 10 | this.threshold = threshold || Fees.INIT_THRESHOLD 11 | this.cap = cap || Fees.INIT_CAP 12 | 13 | this.compute() 14 | } 15 | 16 | Fees.INIT_THRESHOLD = 250000 17 | Fees.INIT_CAP = 200000 18 | Fees.INIT_PERCENT = 0.015 19 | Fees.INIT_ADDITIONAL_CHARGE = 10000 20 | 21 | Fees.prototype.resetDefaults = function () { 22 | 23 | } 24 | 25 | Fees.prototype.withAdditionalCharge = function (additionalCharge) { 26 | this.additionalCharge = additionalCharge 27 | return this 28 | } 29 | 30 | Fees.prototype.withThreshold = function (threshold) { 31 | this.threshold = threshold 32 | return this 33 | } 34 | 35 | Fees.prototype.withCap = function (cap) { 36 | this.cap = cap 37 | return this 38 | } 39 | 40 | Fees.prototype.compute = function () { 41 | this.chargeDivider = this.__chargeDivider() 42 | this.crossover = this.__crossover() 43 | this.flatlinePlusCharge = this.__flatlinePlusCharge() 44 | this.flatline = this.__flatline() 45 | } 46 | 47 | Fees.prototype.__chargeDivider = function () { 48 | return 1 - this.percentage 49 | } 50 | 51 | Fees.prototype.__crossover = function () { 52 | return (this.threshold * this.chargeDivider) - this.additionalCharge 53 | } 54 | 55 | Fees.prototype.__flatlinePlusCharge = function () { 56 | return (this.cap - this.additionalCharge) / this.percentage 57 | } 58 | 59 | Fees.prototype.__flatline = function () { 60 | return this.flatlinePlusCharge - this.cap 61 | } 62 | 63 | Fees.prototype.addFor = function (amountinkobo) { 64 | this.compute() 65 | 66 | if (amountinkobo > this.flatline) { 67 | return parseInt(Math.ceil(amountinkobo + this.cap)) 68 | } else if (amountinkobo > this.crossover) { 69 | return parseInt(Math.ceil((amountinkobo + this.additionalCharge) / this.chargeDivider)) 70 | } else { 71 | return parseInt(Math.ceil(amountinkobo / this.chargeDivider)) 72 | } 73 | } 74 | 75 | Fees.prototype.calculateFor = function (amountinkobo) { 76 | this.compute() 77 | 78 | let fee = this.percentage * amountinkobo 79 | if (amountinkobo >= this.threshold) { 80 | fee += this.additionalCharge 81 | } 82 | 83 | if (fee > this.cap) { 84 | fee = this.cap 85 | } 86 | 87 | return parseInt(Math.ceil(fee)) 88 | } 89 | 90 | PayStack.Fees = Fees 91 | 92 | module.exports = PayStack 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paystack-node", 3 | "version": "0.3.0", 4 | "description": "A NodeJS wrapper for the Paystack API", 5 | "main": "index.js", 6 | "files": [ 7 | "src", 8 | "index.js" 9 | ], 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "lint": "standard --fix", 15 | "test": "mocha \"test/*.test.js\"" 16 | }, 17 | "keywords": [ 18 | "paystack", 19 | "payments", 20 | "nodejs", 21 | "api-endpoints", 22 | "rest-api" 23 | ], 24 | "author": "StitchNG (https://twitter.com/stitchappn)", 25 | "license": "MIT", 26 | "standard": { 27 | "globals": [ 28 | "describe", 29 | "it" 30 | ] 31 | }, 32 | "dependencies": { 33 | "got": "^9.3.2", 34 | "lodash": "^4.17.21" 35 | }, 36 | "devDependencies": { 37 | "chai": "^4.2.0", 38 | "mocha": "^5.2.0", 39 | "standard": "^12.0.1" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/stitchng/paystack.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/stitchng/paystack/issues" 47 | }, 48 | "homepage": "https://github.com/stitchng/paystack#readme" 49 | } 50 | -------------------------------------------------------------------------------- /src/PayStack/extension/Mockable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const payStackMockFactory = require('./facade/MockFactory.js') 4 | 5 | class Mockable { 6 | static engageMock () { 7 | this.prototype._mock = payStackMockFactory.make( 8 | this.prototype.httpClientBaseOptions.hooks, 9 | Object.keys(this.prototype).filter( 10 | prop => (((this.excludeOnMock || []).concat([ 11 | '_mock' 12 | ])).indexOf(prop) === -1) 13 | ) 14 | ) 15 | } 16 | 17 | static respondWithError () { 18 | if (this.prototype._mock !== null) { 19 | this.prototype._mock['_respondWithError'] = true 20 | } 21 | } 22 | 23 | static respondWithoutError () { 24 | if (this.prototype._mock !== null) { 25 | this.prototype._mock['_respondWithError'] = false 26 | } 27 | } 28 | 29 | static disengageMock () { 30 | this.prototype._mock = null 31 | } 32 | 33 | static mockMacro (methodName = '', methodRoutine) { 34 | if (typeof methodName !== 'string') { 35 | return new TypeError('mock method name is not a string') 36 | } 37 | 38 | if (typeof this.prototype[methodName] !== 'function') { 39 | throw new Error('Cannot monkey-patch non-existing methods on mock object') 40 | } 41 | 42 | if (typeof methodRoutine !== 'function') { 43 | throw new TypeError('mock method for mock object is not a function') 44 | } 45 | 46 | if (this.prototype._mock === null) { 47 | throw new Error('call engageMock() first') 48 | } 49 | return (this.prototype._mock[methodName] = methodRoutine) 50 | } 51 | } 52 | 53 | Mockable.prototype._mock = null 54 | 55 | module.exports = Mockable 56 | -------------------------------------------------------------------------------- /src/PayStack/extension/facade/MockFactory.js: -------------------------------------------------------------------------------- 1 | 'use strcit' 2 | 3 | module.exports = { 4 | make: function (hooks = {}, methods = []) { 5 | const mockedMethods = methods.map(function (method) { 6 | let mock = {} 7 | mock[method] = function () { 8 | const that = this 9 | return new Promise(function resolver (resolve, reject) { 10 | const args = Array.prototype.slice.call(arguments) 11 | setTimeout(function timeoutCallback (data) { 12 | if (that._respondWithError) { 13 | const err = new Error('[Paystack] : something unexpected happened') 14 | err.response = { 15 | status: 401, 16 | body: { 17 | status: false, 18 | message: 'error on request to paystack' 19 | } 20 | } 21 | return reject(hooks.onError( 22 | err 23 | )) 24 | } 25 | return resolve({ 26 | status: 200, 27 | statusText: 'OK', 28 | body: { 29 | status: true, 30 | message: 'successfull paystack request {' + method + '}', 31 | data: data || { 32 | status: 'success' 33 | } 34 | } 35 | }) 36 | }, 750, args[0]) 37 | }) 38 | } 39 | return mock 40 | }) 41 | 42 | mockedMethods.unshift({}) 43 | return Object.assign.apply(Object, mockedMethods) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PayStack/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const got = require('got') 4 | const querystring = require('querystring') 5 | const _ = require('lodash') 6 | 7 | const customers = require('../endpoints/customers.js') 8 | const disputes = require('../endpoints/disputes.js') 9 | const transactions = require('../endpoints/transactions.js') 10 | const subaccounts = require('../endpoints/subaccounts.js') 11 | const plans = require('../endpoints/plans.js') 12 | const pages = require('../endpoints/pages.js') 13 | const products = require('../endpoints/products.js') 14 | const balanceHistory = require('../endpoints/balance_history.js') 15 | const transferRecipients = require('../endpoints/transfers_recipients.js') 16 | const refunds = require('../endpoints/refunds.js') 17 | const charges = require('../endpoints/charges.js') 18 | const invoices = require('../endpoints/invoices.js') 19 | const transfers = require('../endpoints/transfers.js') 20 | const verifications = require('../endpoints/verifications.js') 21 | const dedicatedNuban = require('../endpoints/dedicated_nuban.js') 22 | const miscellaneous = require('../endpoints/miscellaneous.js') 23 | const settlements = require('../endpoints/settlements.js') 24 | const subscriptions = require('../endpoints/subscriptions.js') 25 | const controlPanelForSessions = require('../endpoints/control_panel_for_sessions.js') 26 | 27 | const Mockable = require('./extension/Mockable.js') 28 | 29 | /* Any param with '$' at the end is a REQUIRED param both for request body param(s) and request route params */ 30 | const apiEndpoints = Object.assign( 31 | {}, 32 | customers, 33 | disputes, 34 | transactions, 35 | subaccounts, 36 | plans, 37 | pages, 38 | products, 39 | balanceHistory, 40 | refunds, 41 | charges, 42 | invoices, 43 | transfers, 44 | verifications, 45 | miscellaneous, 46 | dedicatedNuban, 47 | settlements, 48 | subscriptions, 49 | transferRecipients, 50 | controlPanelForSessions 51 | ) 52 | 53 | /*! 54 | * 55 | * Provides a convenience extension to _.isEmpty which allows for 56 | * determining an object as being empty based on either the default 57 | * implementation or by evaluating each property to undefined, in 58 | * which case the object is considered empty. 59 | */ 60 | _.mixin(function () { 61 | // reference the original implementation 62 | var _isEmpty = _.isEmpty 63 | return { 64 | // If defined is true, and value is an object, object is considered 65 | // to be empty if all properties are undefined, otherwise the default 66 | // implementation is invoked. 67 | isEmpty: function (value, defined) { 68 | if (defined && _.isObject(value)) { 69 | return !_.some(value, function (value, key) { 70 | return value !== undefined 71 | }) 72 | } 73 | return _isEmpty(value) 74 | } 75 | } 76 | }()) 77 | 78 | /** 79 | * check if a variable literal (or not) is falsy or not 80 | */ 81 | const isLiteralFalsey = (variable) => { 82 | return variable === '' || variable === false || variable === 0 83 | } 84 | 85 | /** 86 | * check if a variable is a reference type (not a literal) or not 87 | */ 88 | 89 | const isPrimitive = (arg) => { 90 | return typeof arg === 'object' || (Boolean(arg) && typeof arg === 'function') 91 | } 92 | 93 | /** 94 | * retrieve the type name either from a reference variables' constructor name or up its' prototype chain 95 | */ 96 | const getNameByChain = (_type, depth) => { 97 | let $depth = depth; 98 | 99 | if(typeof $depth !== 'number'){ 100 | $depth = 1; 101 | } 102 | 103 | let name = _type.name || _type.displayName 104 | const chainList = [(_type.__proto__ && _type.__proto__.prototype)]; 105 | 106 | while(Boolean(chainList[0]) && $depth !== 0){ 107 | const variable = chainList[0].constructor; 108 | if(typeof variable !== 'function'){ 109 | break; 110 | } 111 | name = variable.name || variable.displayName 112 | --depth 113 | chainList.unshift((variable.__proto__ && variable.__proto__.prototype)); 114 | } 115 | chainList.length = 0 116 | 117 | return name 118 | } 119 | 120 | /** 121 | * provide the name of primitive and/or reference types 122 | */ 123 | const checkTypeName = (target, type) => { 124 | let typeName = '' 125 | let match = false 126 | let depth = 0 127 | const MAX_DEPTH = 3 128 | 129 | if(typeof type === 'function'){ 130 | type = (getNameByChain(type, depth)).toLowerCase() 131 | } 132 | 133 | if (isLiteralFalsey(target) || !isPrimitive(target)) { 134 | typeName = typeof target 135 | } else { 136 | typeName = (Object.prototype.toString.call(target)).replace(/^\[object (.+)\]$/, '$1') 137 | } 138 | 139 | match = Boolean(typeName.toLowerCase().indexOf(type) + 1) 140 | 141 | while(!match){ 142 | ++depth; 143 | if(depth === MAX_DEPTH){ 144 | break; 145 | } 146 | 147 | typeName = '' + (target && getNameByChain(target.constructor, depth)) 148 | match = Boolean(typeName.toLowerCase().indexOf(type) + 1) 149 | } 150 | 151 | return match; 152 | } 153 | 154 | /** 155 | * get the actual type of a variable 156 | */ 157 | 158 | /*! 159 | * @EXAMPLES: 160 | * 161 | * strictTypeOf([], 'Array'); // true 162 | * strictTypeOf({}, 'object'); // true 163 | * strictTypeOf(null, 'null'); // true 164 | * strictTypeOf(window.localStorage, Storage); // true 165 | * strictTypeOf('hello!', 'Boolean'); // false 166 | * strictTypeOf(new URL('/', window.location.origin), 'url'); // true 167 | * strictTypeOf(0x35, ['number', 'string']); // true 168 | * strictTypeOf("200,000", ['number', 'string']); // true 169 | */ 170 | const strictTypeOf = (value, type) => { 171 | let result = false 172 | 173 | type = type || [] 174 | 175 | if (typeof type === 'object') { 176 | if (typeof type.length !== 'number') { 177 | return result 178 | } 179 | 180 | let bitPiece = 0 181 | 182 | type = [].slice.call(type) 183 | 184 | type.forEach(_type => { 185 | var localResult = false; 186 | if (typeof _type === 'function') { 187 | localResult = value instanceof _type 188 | } 189 | bitPiece |= Number(localResult || checkTypeName(value, _type.toLowerCase())) 190 | }) 191 | result = Boolean(bitPiece) 192 | } else { 193 | if (typeof type === 'function') { 194 | result = value instanceof type 195 | } 196 | result = result || checkTypeName(value, type.toLowerCase()) 197 | } 198 | return result 199 | } 200 | 201 | const matcherValid = (value, matcher = () => true) => { 202 | return matcher(value); 203 | } 204 | 205 | const isNullOrUndefined = (value) => { 206 | return strictTypeOf(value, ['undefined', 'null']) 207 | } 208 | 209 | const isNumeric = (value) => { 210 | if (strictTypeOf(value, ['string', 'number'])) { 211 | return strictTypeOf(Math.abs(-value), 'number') 212 | } 213 | return false 214 | } 215 | 216 | const objectToJSONString = (value) => { 217 | const isValidObject = value instanceof Object; 218 | if (!value || !isValidObject) { 219 | throw new TypeError( 220 | "objectToJSONString(...): argument 1 is not a valid object" 221 | ) 222 | } 223 | 224 | const isDateObject = value instanceof Date; 225 | 226 | if (!isDateObject && ('toJSON' in value)) { 227 | return value.toJSON(); 228 | } 229 | 230 | return isDateObject 231 | ? value.toISOString().replace(/Z$/, '') 232 | : JSON.stringify(value); 233 | }; 234 | 235 | 236 | const setPathName = (config, values) => { 237 | return config.path.replace(/\{:([\w]+)\}/g, function ( 238 | match, 239 | string, 240 | offset) { 241 | let _value = values[string] || ( 242 | strictTypeOf(config.alternate_route_params_keymap, 'object') 243 | ? values[config.alternate_route_params_keymap[string]] 244 | : false 245 | ); 246 | 247 | if (config.route_params_numeric === true) { 248 | if (!isNumeric(_value)) { 249 | return null 250 | } 251 | } 252 | return strictTypeOf( 253 | _value, 254 | (config.route_params[string] || String) 255 | ) 256 | ? _value 257 | : null 258 | }) 259 | } 260 | 261 | const _jsonify = (data) => { 262 | if (isNullOrUndefined(data)) { 263 | return 'null'; 264 | } 265 | 266 | return typeof data === 'object' 267 | ? objectToJSONString(data) 268 | : String(data) 269 | } 270 | 271 | const setInputValues = (config, inputs) => { 272 | let httpReqOptions = {} 273 | let inputValues = {} 274 | let label = '' 275 | 276 | switch (config.method) { 277 | case 'GET': 278 | case 'HEAD': 279 | case 'DELETE': 280 | label = 'query' 281 | break 282 | 283 | case 'POST': 284 | case 'PUT': 285 | case 'PATCH': 286 | label = 'body' 287 | break 288 | } 289 | 290 | httpReqOptions[label] = {} 291 | 292 | if (config.param_defaults) { 293 | inputs = Object.assign({}, config.param_defaults, inputs) 294 | } 295 | 296 | for (var input in config.params) { 297 | if (config.params.hasOwnProperty(input)) { 298 | let param = input.replace('$', '') 299 | let _input = inputs[param] 300 | let _type = config.params[input] 301 | let _required = false 302 | 303 | if ((input.indexOf('$') + 1) === (input.length)) { 304 | _required = true 305 | } 306 | 307 | if (isNullOrUndefined(_input) || _input === '') { 308 | if (_required) { throw new Error(`param: "${param}" is required but not provided; please provide as needed`) } 309 | } else { 310 | if (!strictTypeOf(_input, _type)) { 311 | throw new Error( 312 | `param: "${param}" is not of the correct type "${_type.toLowerCase()}"; please provide as needed` 313 | ); 314 | } 315 | 316 | httpReqOptions[label][param] = matcherValid( 317 | _input, 318 | 'param_matchers' in config ? config.param_matchers[param] : (() => true) 319 | ) 320 | ? (label === 'query' 321 | ? querystring.escape(_jsonify(_input)) 322 | : _jsonify(_input)) 323 | : null 324 | 325 | if (httpReqOptions[label][param] === null) { 326 | throw new TypeError(`param: "${param}" value: '${_input}' did not pass matcher; please provide as needed`) 327 | } 328 | } 329 | } 330 | } 331 | 332 | inputValues[label] = (label === 'body' 333 | ? (config.send_form 334 | ? httpReqOptions[label] 335 | : JSON.stringify(httpReqOptions[label]) 336 | ) 337 | : querystring.stringify(httpReqOptions[label])) 338 | 339 | return inputValues 340 | } 341 | 342 | const makeMethod = function (config, methodName) { 343 | let httpConfig = { 344 | headers: { 345 | 'Cache-Control': 'no-cache', 346 | 'Accept': 'application/json' 347 | }, 348 | json: true 349 | } 350 | 351 | if (config.send_json) { 352 | httpConfig.headers['Content-Type'] = 'application/json' 353 | httpConfig.form = false 354 | } else if (config.send_form) { 355 | httpConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded' 356 | httpConfig.form = true 357 | } 358 | 359 | return function (requestParams = {}) { 360 | let pathname = config.path 361 | let payload = false 362 | 363 | if (!(requestParams instanceof Object)) { 364 | throw new TypeError( 365 | 'Argument: [ requestParam(s) ] Should Be An Object Literal' 366 | ) 367 | } 368 | 369 | if (!_.isEmpty(requestParams, true)) { 370 | if (config.params !== null) { 371 | payload = setInputValues(config, requestParams) 372 | } 373 | 374 | if (config.route_params !== null) { 375 | pathname = setPathName(config, requestParams) 376 | } 377 | } else { 378 | if (config.params !== null || config.route_params !== null) { 379 | throw new TypeError( 380 | 'Argument: [ requestParam(s) ] Should Not Be Empty!' 381 | ) 382 | } 383 | } 384 | 385 | if (payload === false) { 386 | payload = {} 387 | } 388 | 389 | let reqBody = {} 390 | 391 | for (let type in payload) { 392 | if (payload.hasOwnProperty(type)) { 393 | reqBody = httpConfig[type] = (type === 'query') 394 | ? payload[type] 395 | : JSON.parse(payload[type]) 396 | break 397 | } 398 | } 399 | 400 | const reqVerb = config.method.toLowerCase() 401 | 402 | const canInvokeTestingMock = ( 403 | this._mock !== null && 404 | typeof this._mock[methodName] === 'function' 405 | ) 406 | 407 | if (canInvokeTestingMock) { 408 | if (methodName !== 'chargeBank' && 409 | methodName !== 'chargeCard') { 410 | return this._mock[methodName]( 411 | Object.assign( 412 | httpConfig, 413 | { 'method': config.method } 414 | )) 415 | } else if (isTypeOf(reqBody.card, Object) || 416 | isTypeOf(reqBody.bank, Object)) { 417 | /* eslint-disable camelcase */ 418 | const { cvv, expiry_month, expiry_year } = reqBody.card 419 | const { code, account_number } = reqBody.bank 420 | /* eslint-enable camelcase */ 421 | 422 | // Visa OR Verve 423 | const isTestCardPan = /^408408(4084084081|0000000409|0000005408)$/.test(reqBody.card.number) 424 | const isTestCardCVV = String(cvv) === '408' 425 | const isTestCardExpiry = String(expiry_month) === '02' && String(expiry_year) === '22' 426 | const isTestCard = (isTestCardPan && isTestCardCVV && isTestCardExpiry) 427 | 428 | // Zenith Bank OR First Bank 429 | const isTestBankCode = /^(?:057|011)$/.test(String(code)) 430 | /* eslint-disable-next-line camelcase */ 431 | const isTestBankAccount = account_number === '0000000000' 432 | const isTestBank = (isTestBankCode && isTestBankAccount) 433 | 434 | if (!isTestCard || !isTestBank) { 435 | return this._mock[methodName]( 436 | Object.assign( 437 | httpConfig, 438 | { 'method': config.method } 439 | ) 440 | ) 441 | } 442 | } 443 | } 444 | return this.httpBaseClient[reqVerb](pathname, httpConfig) 445 | } 446 | } 447 | 448 | class PayStack extends Mockable { 449 | get httpClientBaseOptions () { 450 | return { 451 | headers: { }, 452 | hooks: { 453 | beforeResponse: [ 454 | async options => { 455 | // console.log(options) 456 | } 457 | ], 458 | onError: [ 459 | error => { 460 | const { response } = error 461 | if (response && response.body) { 462 | error.name = 'PayStackError' 463 | error.message = `${response.body.message} (${error.statusCode})` 464 | } 465 | 466 | return error 467 | } 468 | ], 469 | afterResponse: [ 470 | (response) => { 471 | let errorMessage = '' 472 | switch (response.statusCode) { 473 | case 400: // Bad Request 474 | errorMessage = 'Request was badly formed | Bad Request (400)' 475 | break 476 | case 401: // Unauthorized 477 | errorMessage = 'Bearer Authorization header may not have been set | Unauthorized (401)' 478 | break 479 | case 404: // Not Found 480 | errorMessage = 'Request endpoint does not exist | Not Found (404)' 481 | break 482 | case 403: // Forbidden 483 | errorMessage = 'Request endpoint requires further priviledges to be accessed | Forbidden (403)' 484 | break 485 | } 486 | 487 | if (response.body && response.body.status === false) { 488 | errorMessage += '; {' + response.body.message + '}' 489 | } 490 | 491 | if (errorMessage !== '') { 492 | const error = new Error(errorMessage) 493 | if (response._isMocked) { 494 | error.response = response 495 | } 496 | error.name = 'PayStackAPIError' 497 | throw error 498 | } 499 | 500 | return response 501 | } 502 | ] 503 | }, 504 | mutableDefaults: false 505 | } 506 | } 507 | 508 | constructor (apiKey, appEnv = 'development') { 509 | super() 510 | const environment = /^(?:development|local|dev)$/ 511 | 512 | const apiBase = { 513 | sandbox: 'https://api.paystack.co', 514 | live: 'https://api.paystack.co' 515 | } 516 | 517 | const clientOptions = this.httpClientBaseOptions 518 | 519 | clientOptions.baseUrl = environment.test(appEnv) ? apiBase.sandbox : apiBase.live 520 | clientOptions.headers['Authorization'] = `Bearer ${apiKey}` 521 | 522 | this.httpBaseClient = got.extend(clientOptions) 523 | } 524 | 525 | mergeNewOptions (newOptions) { 526 | this.httpBaseClient = this.httpBaseClient.extend( 527 | newOptions 528 | ) 529 | } 530 | } 531 | 532 | for (let methodName in apiEndpoints) { 533 | if (apiEndpoints.hasOwnProperty(methodName)) { 534 | PayStack.prototype[methodName] = makeMethod(apiEndpoints[methodName], methodName) 535 | } 536 | } 537 | 538 | PayStack.excludeOnMock = [ 539 | 'httpClientBaseOptions', 540 | 'version' 541 | ] 542 | 543 | module.exports = PayStack 544 | -------------------------------------------------------------------------------- /src/endpoints/balance_history.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Get Balance History 6 | @params: from, to, type 7 | */ 8 | getBalanceHistory: { 9 | method: 'GET', 10 | path: '/balance/ledger', 11 | send_json: false, 12 | params: { from: Date, to: Date, type: String }, 13 | param_defaults: { type: 'Transaction' }, 14 | route_params: null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/endpoints/bulk_charges.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* Charging banks & cards are split into two separate method APIs */ 4 | 5 | module.exports = { 6 | /* 7 | Bulk Charge 8 | @param: (array of objects) 9 | */ 10 | bulkCharge: { 11 | method: 'POST', 12 | path: '/bulkcharge', 13 | send_json: true, 14 | params: { }, 15 | param_defaults: null, 16 | route_params: null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/endpoints/charges.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* Charging banks & cards are split into two separate method APIs */ 4 | 5 | module.exports = { 6 | /* 7 | Charge Card 8 | @param: card, metadata, reference, amount, email 9 | */ 10 | chargeCard: { 11 | method: 'POST', 12 | path: '/charge', 13 | send_json: true, 14 | params: { card$: Object, metadata: Object, reference: String, pin: String, authorization_code: String, device_id: String, amount$: String, email$: String }, 15 | param_defaults: null, 16 | route_params: null 17 | }, 18 | 19 | /* 20 | Charge USSD 21 | @param: ussd, metadata, reference, amount, email 22 | */ 23 | chargeUssd: { 24 | method: 'POST', 25 | path: '/charge', 26 | send_json: true, 27 | params: { ussd$: Object, metadata: Object, reference: String, pin: String, authorization_code: String, device_id: String, amount$: String, email$: String }, 28 | param_defaults: { ussd: { type: '737' } }, 29 | route_params: null 30 | }, 31 | 32 | /* 33 | Charge Mobile Money 34 | @param: mobile_money, currency, metadata, reference, amount, email 35 | */ 36 | chargeMobileMoney: { 37 | method: 'POST', 38 | path: '/charge', 39 | send_json: true, 40 | params: { mobile_money$: Object, currency: String, metadata: Object, reference: String, pin: String, authorization_code: String, device_id: String, amount$: String, email$: String }, 41 | param_defaults: null, 42 | route_params: null 43 | }, 44 | 45 | /* 46 | Charge Bank 47 | @param: bank, metadata, reference, amount, email 48 | */ 49 | chargeBank: { 50 | method: 'POST', 51 | path: '/charge', 52 | send_json: true, 53 | params: { bank$: Object, metadata: Object, reference: String, pin: String, authorization_code: String, mobile_money: Object, device_id: String, amount$: String, email$: String }, 54 | route_params: null 55 | }, 56 | 57 | /* 58 | Submit PIN 59 | @param: pin, reference 60 | */ 61 | submitPIN: { 62 | method: 'POST', 63 | path: '/charge/submit_pin', 64 | send_json: true, 65 | params: { pin$: String, reference$: String }, 66 | route_params: null 67 | }, 68 | 69 | /* 70 | Submit OTP 71 | @param: otp, reference 72 | */ 73 | submitOTP: { 74 | method: 'POST', 75 | path: '/charge/submit_otp', 76 | send_json: true, 77 | params: { otp$: String, reference$: String }, 78 | route_params: null 79 | }, 80 | 81 | /* 82 | Submit Phone 83 | @param: phone, reference 84 | */ 85 | submitPhone: { 86 | method: 'POST', 87 | path: '/charge/submit_phone', 88 | send_json: true, 89 | params: { phone$: String, reference$: String }, 90 | route_params: null 91 | }, 92 | 93 | /* 94 | Submit Birthday 95 | @param: birthday, reference 96 | */ 97 | submitBirthday: { 98 | method: 'POST', 99 | path: '/charge/submit_birthday', 100 | send_json: true, 101 | params: { birthday$: Date, reference$: String }, 102 | route_params: null 103 | }, 104 | 105 | /* 106 | Submit Address 107 | @param: address, reference, city, state, zipcode 108 | */ 109 | submitAddress: { 110 | method: 'POST', 111 | path: '/charge/submit_address', 112 | send_json: true, 113 | params: { address$: String, reference$: String, city$: String, state$: String, zipcode: String }, 114 | route_params: null 115 | }, 116 | 117 | /* 118 | Check Pending Charge 119 | @param: reference 120 | */ 121 | checkPendingCharge: { 122 | method: 'GET', 123 | path: '/charge/{:reference}', 124 | send_json: false, 125 | params: null, 126 | param_defaults: null, 127 | route_params: { reference: String } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/endpoints/control_panel_for_sessions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Get Payment session timeout 6 | @params: 7 | */ 8 | getPaymentSessionTimeout: { 9 | method: 'GET', 10 | path: '/integration/payment_session_timeout', 11 | send_json: false, 12 | params: null, 13 | route_params: null 14 | }, 15 | 16 | /* 17 | Update Payment session timeout 18 | @params: timeout 19 | */ 20 | updatePaymentSessionTimeout: { 21 | method: 'PUT', 22 | path: '/integration/payment_session_timeout', 23 | send_json: true, 24 | params: { timeout: Number }, 25 | param_defaults: { timeout: 0 }, 26 | route_params: null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/endpoints/customers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create customer 6 | @params: first_name, last_name, email(required), phone 7 | */ 8 | createCustomer: { 9 | method: 'POST', 10 | path: '/customer', 11 | send_json: true, 12 | params: { first_name: String, last_name: String, email$: String, phone: String, metadata: Object }, 13 | route_params: null 14 | }, 15 | 16 | /* 17 | Get customer 18 | @params: customer_id 19 | */ 20 | getCustomer: { 21 | method: 'GET', 22 | alternate_route_params_keymap: { email_or_id_or_customer_code: 'customer_id' }, 23 | path: '/customer/{:email_or_id_or_customer_code}', 24 | send_json: false, 25 | params: null, 26 | route_params: { email_or_id_or_customer_code: String } 27 | }, 28 | 29 | /* 30 | List customers 31 | @params: perPage, page 32 | */ 33 | listCustomers: { 34 | method: 'GET', 35 | path: '/customer', 36 | params: { perPage: Number, page: Number }, 37 | param_defaults: { perPage: 50, page: 1 }, 38 | route_params: null 39 | }, 40 | 41 | /* 42 | Update customer 43 | @params: id_or_customer_code, first_name, last_name, email (required), phone 44 | */ 45 | updateCustomer: { 46 | method: 'PUT', 47 | alternate_route_params_keymap: { id_or_customer_code: 'customer_id' }, 48 | path: '/customer/{:id_or_customer_code}', 49 | send_json: true, 50 | params: { first_name: String, last_name: String, email$: String, phone: String, metadata: Object }, 51 | route_params: { id_or_customer_code: String } 52 | }, 53 | 54 | /* 55 | Deactivate Customer Authoorization 56 | @params: authorization_code 57 | */ 58 | deactivateAuthOnCustomer: { 59 | method: 'POST', 60 | path: '/customer/deactivate_authorization', 61 | send_json: true, 62 | params: { authorization_code: String }, 63 | route_params: null 64 | }, 65 | 66 | /* 67 | White/Blacklist Customer 68 | @params: customer (required), risk_action 69 | 70 | @info: [ 'allow' to whitelist or 'deny' to blacklist ] 71 | */ 72 | setRiskActionOnCustomer: { 73 | method: 'POST', 74 | path: '/customer/set_risk_action', 75 | send_json: true, 76 | params: { customer$: String, risk_action: String }, 77 | param_defaults: { risk_action: 'allow' }, 78 | route_params: null 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/endpoints/dedicated_nuban.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Dedicated Nuban 6 | @params: customer, prefered_bank 7 | */ 8 | createDedicatedNuban: { 9 | method: 'POST', 10 | path: '/dedicated_account', 11 | send_json: true, 12 | params: { $customer: String, preferred_bank: String }, 13 | param_defaults: { preferred_bank: 'wema-bank' }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Dedicated Nubans 19 | @params: customer, provider_slug, active, currency, bank_id 20 | */ 21 | listDedicatedNubans: { 22 | method: 'GET', 23 | path: '/dedicated_account', 24 | send_json: false, 25 | params: { customer: String, provider_slug: String, $active: Boolean, $currency: String, bank_id: String }, 26 | param_defaults: { provider_slug: 'wema-bank' }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch Dedicated Nuban 32 | @params: dedicated_account_id 33 | */ 34 | fetchDedicatedNuban: { 35 | method: 'GET', 36 | path: '/dedicated_account/{:dedicated_account_id}', 37 | send_json: false, 38 | params: null, 39 | route_params: { dedicated_account_id: String } 40 | }, 41 | 42 | /* 43 | Deactivate Dedicated Nuban 44 | @params: dedicated_account_id 45 | */ 46 | deactivateDedicatedNuban: { 47 | method: 'DELETE', 48 | path: '/dedicated_account/{:dedicated_account_id}', 49 | send_json: false, 50 | params: null, 51 | route_params: { dedicated_account_id: String } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/endpoints/disputes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | List Disputes 6 | @params: from, to, page, perPage, transaction 7 | */ 8 | listDisputes: { 9 | method: 'GET', 10 | path: '/dispute', 11 | send_json: false, 12 | params: { from: Date, to: Date, page: Number, perPage: Number, transaction: Number }, 13 | param_defaults: { page: 1, perPage: 10 }, 14 | route_params: null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/endpoints/invoices.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create an Invoice 6 | @params: description, line_items, tax, customer, amount, due_date, draft, has_invoice, metadata 7 | */ 8 | createInvoice: { 9 | method: 'POST', 10 | path: '/paymentrequest', 11 | send_json: true, 12 | params: { description: String, line_items: Array, tax: Array, customer$: String, amount$: Number, due_date$: String, draft: Boolean, has_invoice: Boolean, metadata: Object, send_notification: Boolean }, 13 | param_defaults: { draft: false, has_invoice: false, metadata: {} }, 14 | route_params: null 15 | }, 16 | /* 17 | View an Invoice 18 | @params: invoice_id_or_code 19 | */ 20 | viewInvoice: { 21 | method: 'GET', 22 | path: '/paymentrequest/{:invoice_id_or_code}', 23 | send_json: false, 24 | params: null, 25 | route_params: { invoice_id_or_code: String } 26 | }, 27 | /* 28 | Update an Invoice 29 | @params: description, line_items, customer , due_date, metadata, send_notification 30 | */ 31 | updateInvoice: { 32 | method: 'PUT', 33 | path: '/paymentrequest/{:invoice_id_or_code}', 34 | send_json: true, 35 | params: { description: String, line_items: Array, tax: Array, customer: String, due_date: String, metadata: Object, send_notification: Boolean }, 36 | param_defaults: { send_notification: false }, 37 | route_params: { invoice_id_or_code: String } 38 | }, 39 | 40 | /* 41 | List All Invoices 42 | @params: customer , status, currency, paid, include_archive 43 | */ 44 | listInvoices: { 45 | method: 'GET', 46 | path: '/paymentrequest', 47 | send_json: false, 48 | params: { customer: String, status: String, currency: String, paid: String, include_archive: String }, 49 | param_defaults: { currency: 'NGN' }, 50 | route_params: null 51 | }, 52 | 53 | /* 54 | Verify Invoice 55 | @params: invoice_code 56 | */ 57 | verifyInvoice: { 58 | method: 'GET', 59 | path: '/paymentrequest/verify/{:invoice_code}', 60 | send_json: false, 61 | params: null, 62 | param_defaults: null, 63 | route_params: { invoice_code: String } 64 | }, 65 | 66 | /* 67 | Finalize [ Invoice ] Draft 68 | @params: id_or_code, send_notification 69 | */ 70 | finalizeInvoiceDraft: { 71 | method: 'POST', 72 | path: '/paymentrequest/finalize/{:id_or_code}', 73 | send_json: true, 74 | params: { send_notification: Boolean }, 75 | param_defaults: { send_notification: true }, 76 | route_params: { id_or_code: String } 77 | }, 78 | 79 | /* 80 | Get [ Invoice ] Metrics 81 | @params: 82 | */ 83 | 84 | getMetricsForInvoices: { 85 | method: 'GET', 86 | path: '/paymentrequest/totals', 87 | send_form: true, 88 | params: null, 89 | param_defaults: null, 90 | route_params: null 91 | }, 92 | 93 | /* 94 | Send [ Invoice ] Notification 95 | @params: id_or_code 96 | */ 97 | sendInvoiceNotification: { 98 | method: 'POST', 99 | path: '/paymentrequest/notify/{:id_or_code}', 100 | send_json: true, 101 | params: null, 102 | route_params: { id_or_code: String } 103 | }, 104 | 105 | /* 106 | Archive Invoice 107 | @params: invoice_id_or_code 108 | */ 109 | archiveInvoice: { 110 | method: 'POST', 111 | path: '/invoice/archive/{:invoice_id_or_code}', 112 | send_form: true, 113 | params: null, 114 | route_params: { invoice_id_or_code: String } 115 | }, 116 | 117 | /* 118 | Mark [ Invoice ] As Piad 119 | @params: id, amount_paid, paid_by, payment_date, payment_method, note 120 | */ 121 | markInvoiceAsPaid: { 122 | method: 'POST', 123 | path: '/paymentrequest/mark_as_paid/{:id}', 124 | send_json: true, 125 | params: { amount_paid$: Number, paid_by$: String, payment_date$: String, payment_method$: String, note: String }, 126 | param_defaults: { payment_method: 'Cash' }, 127 | route_params: { id: String } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/endpoints/miscellaneous.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | List Banks 6 | @param: perPage, page, use_cursor(required), next, previous, gateway, type, country(required) 7 | */ 8 | listBanks: { 9 | method: 'GET', 10 | path: '/bank', 11 | send_json: false, 12 | params: { perPage$: Number, page: Number, use_cursor: Boolean, next: String, previous: String, type: String, currency: String, country$: String }, 13 | param_defaults: { perPage: 50, page: 1, currency: 'NGN', country: 'Nigeria' }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Providers 19 | @param: pay_with_bank_transfer 20 | */ 21 | listProviders: { 22 | method: 'GET', 23 | path: '/bank', 24 | send_json: false, 25 | params: { pay_with_bank_transfer$: Boolean }, 26 | param_defaults: { pay_with_bank_transfer: true }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | List Countries 32 | @param: - 33 | */ 34 | listCountries: { 35 | method: 'GET', 36 | path: '/country', 37 | send_json: false, 38 | params: null, 39 | route_params: null 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/endpoints/pages.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Pages 6 | @params: name(required), description, amount, slug, redirect_url, custom_fields 7 | */ 8 | createPage: { 9 | method: 'POST', 10 | path: '/page', 11 | send_json: true, 12 | params: { name$: String, description: String, amount: Number, slug: String, redirect_url: String, custom_fields: Array }, 13 | route_params: null 14 | }, 15 | /* 16 | List Pages 17 | @params: perPage, page 18 | */ 19 | listPages: { 20 | method: 'GET', 21 | path: '/page', 22 | send_json: false, 23 | params: { perPage: Number, page: Number }, 24 | param_defaults: { perPage: 50, page: 1 }, 25 | route_params: null 26 | }, 27 | /* 28 | Fetch Page 29 | @params: id_or_slug 30 | */ 31 | getPage: { 32 | method: 'GET', 33 | path: '/page/{:id_or_slug}', 34 | send_json: false, 35 | params: null, 36 | route_params: { id_or_slug: String } 37 | }, 38 | /* 39 | Update Pages 40 | @params: id_or_slug, name, description, amount, active 41 | */ 42 | updatePage: { 43 | method: 'PUT', 44 | path: '/page/{:id_or_slug}', 45 | send_json: true, 46 | params: { name$: String, description: String, amount: Number, active: Boolean }, 47 | param_defaults: { active: true }, 48 | route_params: { id_or_slug: String } 49 | }, 50 | 51 | /* 52 | Check Availability Of Slug 53 | @params: slug 54 | */ 55 | checkSlugAvailability: { 56 | method: 'GET', 57 | path: '/page/check_slug_availability/{:slug}', 58 | send_json: false, 59 | params: null, 60 | route_params: { slug: String } 61 | }, 62 | 63 | /* 64 | Add Page Product 65 | @params: id, products 66 | */ 67 | addPageProduct: { 68 | method: 'POST', 69 | path: '/page/{:id}/product', 70 | send_json: true, 71 | params: { products$: Array }, 72 | route_params_numeric: true, 73 | route_params: { id: String } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/endpoints/plans.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Plan 6 | @param: name, interval, currency, amount, send_invoices, send_sms, invoice_limit, description 7 | */ 8 | createPlan: { 9 | method: 'POST', 10 | path: '/plan', 11 | send_json: true, 12 | params: { name$: String, interval$: String, currency: String, amount$: Number, send_invoices: Boolean, send_sms: Boolean, invoice_limit: Number, description: String }, 13 | param_defaults: { interval: 'monthly', currency: 'NGN', send_invoices: false, send_sms: false, invoice_limit: 0 }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Plans 19 | @param: perPage, page 20 | */ 21 | listPlans: { 22 | method: 'GET', 23 | path: '/plan', 24 | params: { perPage: Number, page: Number }, 25 | send_json: false, 26 | param_defaults: { perPage: 0, page: 0 }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch Plan 32 | @param: id_or_plan_code 33 | */ 34 | getPlan: { 35 | method: 'GET', 36 | path: '/plan/{:id_or_plan_code}', 37 | params: null, 38 | send_json: false, 39 | route_params: { id_or_plan_code: String } 40 | }, 41 | 42 | /* 43 | Update Plan 44 | @param: name, interval, currency, amount, send_invoices, send_sms, invoice_limit, description 45 | */ 46 | updatePlan: { 47 | method: 'PUT', 48 | path: '/plan/{:id_or_plan_code}', 49 | send_json: true, 50 | params: { name$: String, currency: String, amount$: Number, send_invoices: Boolean, send_sms: Boolean, invoice_limit: Number, description: String }, 51 | param_defaults: { currency: 'NGN', send_invoices: false, send_sms: false, invoice_limit: 0 }, 52 | route_params: { id_or_plan_code: String } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/endpoints/products.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Product 6 | @param: name, description, price, currency, limited, quantity 7 | */ 8 | createProduct: { 9 | method: 'POST', 10 | path: '/product', 11 | send_json: true, 12 | params: { name$: String, description: String, price$: Number, currency$: String, limited: Boolean, quantity: Number }, 13 | param_defaults: { currency: 'NGN', limited: false }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Products 19 | @param: 20 | */ 21 | listProduct: { 22 | method: 'GET', 23 | path: '/product', 24 | send_json: false, 25 | params: null, 26 | param_defaults: null, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch Product 32 | @param: id 33 | */ 34 | getProduct: { 35 | method: 'GET', 36 | path: '/product/{:id}', 37 | send_json: false, 38 | params: null, 39 | param_defaults: null, 40 | route_params: { id: String } 41 | }, 42 | 43 | /* 44 | Update Product 45 | @param: id, name, description, price, currency, limited, quantity 46 | */ 47 | updateProduct: { 48 | method: 'PUT', 49 | path: '/product/{:id}', 50 | send_json: true, 51 | params: { name$: String, description: String, price$: Number, currency$: String, limited: Boolean, quantity: Number }, 52 | param_defaults: { currency: 'NGN', limited: false }, 53 | route_params: { id: String } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/endpoints/refunds.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Refund 6 | @param: transaction(reference), amount, currency, customer_note, merchant_note 7 | */ 8 | createRefund: { 9 | method: 'POST', 10 | path: '/refund', 11 | send_json: true, 12 | params: { transaction$: String, amount: Number, currency: String, customer_note: String, merchant_note: String }, 13 | param_defaults: { currency: 'NGN' }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Refund 19 | @param: reference(required), currency 20 | */ 21 | listRefund: { 22 | method: 'GET', 23 | path: '/refund', 24 | send_json: false, 25 | params: { reference$: String, currency: String }, 26 | param_defaults: { currency: 'NGN' }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch Refund 32 | @param: reference, currency 33 | */ 34 | getRefund: { 35 | method: 'GET', 36 | path: '/refund/{:reference}', 37 | send_json: false, 38 | params: null, 39 | route_params: { reference: String } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/endpoints/settlements.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Get Settlements 6 | @params: from, to, subaccount 7 | */ 8 | getSettlements: { 9 | method: 'GET', 10 | path: '/settlement', 11 | send_json: false, 12 | params: { from: Date, to: Date, subaccount: String }, 13 | param_defaults: { subaccount: 'none' }, 14 | route_params: null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/endpoints/subaccounts.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Sub Account 6 | @param: business_name, settlement_bank, account_number, percentage_charge, primary_contact_email, primary_contact_name, primary_contact_phone, metadata, settlement_schedule 7 | */ 8 | createSubaccount: { 9 | method: 'POST', 10 | path: '/subaccount', 11 | send_json: true, 12 | params: { business_name$: String, settlement_bank: String, account_number$: String, percentage_charge$: Number, primary_contact_email: String, primary_contact_name: String, primary_contact_phone: String, metadata: String, settlement_schedule: String }, 13 | param_defaults: { settlement_schedule: 'auto', percentage_charge: 0 }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Sub Accounts 19 | @param: perPage(50 per page), page 20 | */ 21 | listSubaccount: { 22 | method: 'GET', 23 | path: '/subaccount', 24 | send_json: false, 25 | params: { perPage: Number, page: Number }, 26 | param_defaults: { perPage: 50, page: 1 }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch Sub Account 32 | @param: id_or_slug 33 | */ 34 | getSubaccount: { 35 | method: 'GET', 36 | path: '/subaccount/{:id_or_slug}', 37 | send_json: false, 38 | params: null, 39 | param_defaults: null, 40 | route_params: { id_or_slug: String } 41 | }, 42 | 43 | /* 44 | Update Sub Account 45 | @param: id_or_slug, business_name, settlement_bank, account_number, percentage_charge, primary_contact_email, primary_contact_name, primary_contact_phone, metadata, settlement_schedule 46 | */ 47 | updateSubaccount: { 48 | method: 'PUT', 49 | path: '/subaccount/{:id_or_slug}', 50 | send_json: true, 51 | params: { business_name: String, settlement_bank: String, account_number: String, percentage_charge: Number, primary_contact_email: String, primary_contact_phone: String, metadata: String, settlement_schedule: String }, 52 | param_defaults: { settlement_schedule: 'auto', percentage_charge: 0 }, 53 | route_params: { id_or_slug: String } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/endpoints/subscriptions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Subscription 6 | @param: customer, plan, authorization, start_date 7 | */ 8 | createSubscription: { 9 | method: 'POST', 10 | path: '/subscription', 11 | send_json: true, 12 | params: { customer$: String, plan$: String, authorization$: String, start_date: Date }, 13 | param_defaults: null, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Subscription 19 | @param: perPage, page, customer, plan 20 | */ 21 | listSubscription: { 22 | method: 'GET', 23 | path: '/subscription', 24 | send_json: false, 25 | params: { perPage: Number, page: Number, customer: Number, plan: Number }, 26 | param_defaults: { perPage: 50, page: 1, customer: 0, plan: 0 }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Disable Subscription 32 | @param: code, token 33 | */ 34 | disableSubscription: { 35 | method: 'POST', 36 | path: '/subscription/disable', 37 | send_json: true, 38 | params: { code: String, token: String }, 39 | param_defaults: null, 40 | route_params: null 41 | }, 42 | 43 | /* 44 | Enable Subscription 45 | @param: code, token 46 | */ 47 | enableSubscription: { 48 | method: 'POST', 49 | path: '/subscription/enable', 50 | send_json: true, 51 | params: { code: String, token: String }, 52 | param_defaults: null, 53 | route_params: null 54 | }, 55 | 56 | /* 57 | Fetch Subscription 58 | @param: id_or_subscription_code 59 | */ 60 | getSubscription: { 61 | method: 'GET', 62 | path: '/subscription/{:id_or_subscription_code}', 63 | params: null, 64 | param_defaults: null, 65 | route_params: { id_or_subscription_code: String } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/endpoints/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Initialize Transaction 6 | @params: reference, callback_url, amount, email, plan, subaccount, transaction_charge, bearer, channels, invoice_limit, metadata 7 | */ 8 | initializeTransaction: { 9 | method: 'POST', 10 | path: '/transaction/initialize', 11 | send_json: true, 12 | params: { reference: String, callback_url: String, amount$: Number, email$: String, plan: String, subaccount: String, transaction_charge: Number, bearer: String, channels: Array, invoice_limit: Number, metadata: String }, 13 | param_defaults: { invoice_limit: 0, bearer: 'account' }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | Verify Transaction 19 | @params: reference 20 | */ 21 | verifyTransaction: { 22 | method: 'GET', 23 | path: '/transaction/verify/{:reference}', 24 | send_json: false, 25 | params: null, 26 | param_defaults: null, 27 | route_params: { reference: String } 28 | }, 29 | 30 | /* 31 | List Transaction 32 | @params: perPage, page, customer, status, from, to, amount 33 | */ 34 | listTransaction: { 35 | method: 'GET', 36 | path: '/transaction', 37 | send_json: false, 38 | params: { perPage: Number, page: Number, customer: Number, status: String, from: Date, to: Date, amount: Number }, 39 | param_defaults: { perPage: 50, page: 1 }, 40 | route_params: null 41 | }, 42 | 43 | /* 44 | Fetch Transaction 45 | @params: id 46 | */ 47 | getTransaction: { 48 | method: 'GET', 49 | path: '/transaction/{:id}', 50 | send_json: false, 51 | param_defaults: null, 52 | params: null, 53 | route_params: { id: String } 54 | }, 55 | 56 | /* 57 | Charge Authorization 58 | @params: reference, authorization_code, amount, plan, currency, email, metadata, subaccount, transaction_charge, bearer, invoice_limit 59 | */ 60 | chargeAuthorization: { 61 | method: 'POST', 62 | path: '/transaction/charge_authorization', 63 | send_json: true, 64 | params: { reference: String, authorization_code$: String, amount$: Number, plan: String, currency: String, email$: String, metadata: Object, subaccount: String, transaction_charge: Number, bearer: String, invoice_limit: Number }, 65 | param_defaults: { amount: 0, currency: 'NGN', bearer: 'account', invoice_limit: 0 }, 66 | route_params: null 67 | }, 68 | 69 | /* 70 | View Transaction Timeline 71 | @params: id 72 | */ 73 | viewTransactionTimeline: { 74 | method: 'GET', 75 | path: '/transaction/timeline/{:id}', 76 | send_json: false, 77 | param_defaults: null, 78 | params: null, 79 | route_params: { id: String } 80 | }, 81 | 82 | /* 83 | Transaction Totals 84 | @params: from, to 85 | */ 86 | transactionTotals: { 87 | method: 'GET', 88 | path: '/transaction/totals', 89 | send_json: false, 90 | params: { from: Date, to: Date }, 91 | param_defaults: null, 92 | route_params: null 93 | }, 94 | 95 | /* 96 | Export Transaction 97 | @params: from, to, settled, payment_page, customer, currency, settlement, amount, status 98 | */ 99 | exportTransaction: { 100 | method: 'GET', 101 | path: '/transaction/export', 102 | send_json: false, 103 | params: { from: Date, to: Date, settled: Boolean, payment_page: Number, customer: Number, currency: String, settlement: Number, amount: Number, status: String }, 104 | param_defaults: { status: 'success' }, 105 | route_params: null 106 | }, 107 | 108 | /* 109 | Request Reauthorization 110 | @params: reference, authorization_code, amount, currency, email, metadata 111 | */ 112 | requestReauthorization: { 113 | method: 'POST', 114 | path: '/transaction/request_reauthorization', 115 | send_json: true, 116 | params: { reference: String, authorization_code$: String, amount$: Number, currency: String, email$: String, metadata: Object }, 117 | param_defaults: { amount: 0, currency: 'NGN' }, 118 | route_params: null 119 | }, 120 | 121 | /* 122 | Check Authorization 123 | @params: authorization_code, currency, email, amount 124 | */ 125 | checkAuthorization: { 126 | method: 'POST', 127 | path: '/transaction/check_authorization', 128 | send_json: true, 129 | params: { authorization_code$: String, amount$: Number, email$: String, currency: String }, 130 | param_defaults: { currency: 'NGN' }, 131 | route_params: null 132 | }, 133 | 134 | /* 135 | Partial Debit 136 | @params: authorization_code, currency, email, amount, at_least, reference 137 | */ 138 | partialDebit: { 139 | method: 'POST', 140 | path: '/transaction/partial_debit', 141 | send_json: true, 142 | params: { authorization_code$: String, amount$: Number, email$: String, currency$: String, at_least: Number, reference: String }, 143 | param_defaults: { currency: 'NGN' }, 144 | route_params: null 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/endpoints/transfers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Initiate Transfer 6 | @param: source(required), reason, amount(required), recipient(required), currency, reference 7 | */ 8 | initiateTransfer: { 9 | method: 'POST', 10 | path: '/transfer', 11 | params: { source$: String, reason: String, amount$: Number, recipient$: String, currency: String, reference: String }, 12 | send_json: true, 13 | param_defaults: { source: 'balance', currency: 'NGN' }, 14 | route_params: null 15 | }, 16 | 17 | /* 18 | List Transfers 19 | @param: perPage, page 20 | */ 21 | listTransfers: { 22 | method: 'GET', 23 | path: '/transfer', 24 | send_json: false, 25 | params: { perPage: Number, page: Number }, 26 | param_defaults: { perPage: 0, page: 0 }, 27 | route_params: null 28 | }, 29 | 30 | /* 31 | Fetch transfer 32 | @param: id_or_code 33 | */ 34 | fetchTransfer: { 35 | method: 'GET', 36 | path: '/transfer/{:id_or_code}', 37 | send_json: false, 38 | params: null, 39 | param_defaults: null, 40 | route_params: { id_or_code: String } 41 | }, 42 | 43 | /* 44 | Finalize Transfer 45 | @param: transfer_code, otp 46 | */ 47 | finalizeTransfer: { 48 | method: 'POST', 49 | path: '/transfer/finalize_transfer', 50 | send_json: true, 51 | params: { transfer_code$: String, otp$: String }, 52 | param_defaults: null, 53 | route_params: null 54 | }, 55 | 56 | /* 57 | Initiate Bulk Transfer 58 | @param: source, currency, transfers 59 | */ 60 | initiateBulkTransfer: { 61 | method: 'POST', 62 | path: '/transfer/bulk', 63 | send_json: true, 64 | params: { source$: String, transfers$: String, currency: String }, 65 | param_defaults: { source: 'balance', currency: 'NGN', transfers: [] }, 66 | route_params: null 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/endpoints/transfers_recipients.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Create Transfer Recipient 6 | @params: type, name, account_number, bank_code, currency, description, authorization_code 7 | */ 8 | createTransferRecipient: { 9 | method: 'POST', 10 | path: '/transferrecipient', 11 | send_json: true, 12 | params: { type$: String, name$: String, account_number$: String, bank_code$: String, currency: String, description: String, authorization_code: String }, 13 | param_defaults: { type: 'nuban', currency: 'NGN' }, 14 | route_params: null 15 | }, 16 | /* 17 | List Transfer Recipients 18 | @params: perPage, page 19 | */ 20 | listTransferRecipients: { 21 | method: 'GET', 22 | path: '/transferrecipient', 23 | send_json: false, 24 | params: { perPage: Number, page: Number }, 25 | param_defaults: { perPage: 0, page: 0 }, 26 | route_params: null 27 | }, 28 | 29 | /* 30 | Update a transfer recipient 31 | @params: name, email, recipient_code_or_id 32 | */ 33 | updateTransferRecipient: { 34 | method: 'PUT', 35 | path: '/transferrecipient/{:recipient_code_or_id}', 36 | send_json: true, 37 | params: { name: String, email: String }, 38 | param_defaults: null, 39 | route_params: { recipient_code_or_id: String } 40 | }, 41 | /* 42 | Delete Transfer Recipient 43 | @params: recipient_code_or_id 44 | */ 45 | deleteTransferRecipient: { 46 | method: 'DELETE', 47 | path: '/transferrecipient/{:recipient_code_or_id}', 48 | send_json: false, 49 | params: null, 50 | param_defaults: null, 51 | route_params: { recipient_code_or_id: String } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/endpoints/verifications.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | Resolve Bank Verification Number (Standard) 6 | @param: bvn 7 | */ 8 | resolveBVN: { 9 | method: 'GET', 10 | path: '/bank/resolve_bvn/{:bvn}', 11 | params: null, 12 | param_defaults: null, 13 | route_params: { bvn: String } 14 | }, 15 | 16 | /* 17 | Resolve Bank Verification Number (Premium) 18 | @param: bvn 19 | */ 20 | resolveBVNPremium: { 21 | method: 'GET', 22 | path: 'identity/bvn/resolve/{:bvn}', 23 | params: null, 24 | param_defaults: null, 25 | route_params: { bvn: String } 26 | }, 27 | 28 | /* 29 | Match Bank Verification Number 30 | @param: bvn (required), account_number(required), bank_code (required), first_name, middle_name, last_name 31 | */ 32 | matchBVN: { 33 | method: 'POST', 34 | path: '/bvn/match', 35 | send_json: true, 36 | params: { bvn$: String, bank_code$: String, account_number$: String, first_name: String, middle_name: String, last_name: String }, 37 | param_defaults: null, 38 | route_params: null 39 | }, 40 | 41 | /* 42 | Resolve Account Number 43 | @param: account_number(required), bank_code (required) 44 | */ 45 | resolveAccountNumber: { 46 | method: 'GET', 47 | path: '/bank/resolve', 48 | params: { account_number$: String, bank_code$: String }, 49 | param_defaults: null, 50 | route_params: null 51 | }, 52 | 53 | /* 54 | Resolve Card Bin 55 | @param: bin 56 | */ 57 | resolveCardBin: { 58 | method: 'GET', 59 | path: '/decision/bin/{:bin}', 60 | params: null, 61 | param_defaults: null, 62 | route_params: { bin: String } 63 | }, 64 | 65 | /* 66 | Resolve Phone Number 67 | @param: verification_type, phone, callback_url 68 | */ 69 | resolvePhoneNumber: { 70 | method: 'POST', 71 | path: '/verifications', 72 | send_json: true, 73 | params: { verification_type$: String, phone$: String, callback_url$: String }, 74 | param_defaults: { verification_type: 'truecaller' }, 75 | route_params: null 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/paystack-object.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var chai = require('chai') 4 | var expect = chai.expect 5 | var should = chai.should() 6 | 7 | describe('PayStack Instance Test(s)', function () { 8 | // Created Instance 9 | var PayStack = require('../index.js') 10 | var instance = new PayStack('sk_test_2hWyQ6HW73jS8p1IkXmSWOlE4y9Inhgyd6g5f2R7') 11 | 12 | it('should have a function [mergeNewOptions]', function () { 13 | // mock not enabled 14 | expect(Object.prototype.toString.call(instance._mock)).to.be.equal('[object Null]') 15 | 16 | PayStack.engageMock() 17 | // mock now enabled 18 | expect(Object.prototype.toString.call(instance._mock)).to.be.equal('[object Object]') 19 | /* eslint-disable no-unused-expressions */ 20 | expect((typeof instance.mergeNewOptions === 'function')).to.be.true 21 | expect((typeof instance.createCustomer === 'function')).to.be.true 22 | expect((typeof instance.listDisputes === 'function')).to.be.true 23 | expect((typeof instance.addPageProduct === 'function')).to.be.true 24 | expect((typeof instance.listBanks === 'function')).to.be.true 25 | expect((typeof instance.listCountries === 'function')).to.be.true 26 | expect((typeof instance.getBalanceHistory === 'function')).to.be.true 27 | 28 | PayStack.disengageMock() 29 | expect(Object.prototype.toString.call(instance._mock)).to.be.equal('[object Null]') 30 | expect((typeof instance.createInvoice === 'function')).to.be.true 31 | expect((typeof instance.createPage === 'function')).to.be.true 32 | expect((typeof instance.chargeBank === 'function')).to.be.true 33 | expect((typeof instance.chargeCard === 'function')).to.be.true 34 | expect((typeof instance.createDedicatedNuban === 'function')).to.be.true 35 | expect((typeof instance.createProduct === 'function')).to.be.true 36 | expect((typeof PayStack.Fees === 'function')).to.be.true 37 | /* eslint-enable no-unused-expressions */ 38 | }) 39 | 40 | it('should throw an error if method is called without required arguments', function () { 41 | try { 42 | instance.createCustomer() 43 | } catch (err) { 44 | should.exist(err) 45 | } 46 | }) 47 | 48 | it('should throw an error if method is called with any arguments other than an object', function () { 49 | try { 50 | instance.createInvoice([]) 51 | } catch (err) { 52 | should.exist(err) 53 | } 54 | }) 55 | }) 56 | --------------------------------------------------------------------------------