├── .gitignore ├── README.md ├── package.json ├── lib └── liqpay.js └── test └── liqpay.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .idea 3 | coverage 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sdk-nodejs 2 | ======= 3 | 4 | LiqPay SDK-NodeJS 5 | 6 | Documentation https://www.liqpay.ua/documentation/en 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liqpay-sdk-nodejs", 3 | "version": "2.0.0", 4 | "description": "Node.js sdk for liqpay.ua api", 5 | "main": "lib/liqpay.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "jest": { 13 | "collectCoverage":true 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "Open Software License (OSL 3.0)", 18 | "dependencies": { 19 | "axios": "^1.4.0", 20 | "jest": "^29.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/liqpay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Liqpay Payment Module 4 | * 5 | * NOTICE OF LICENSE 6 | * 7 | * This source file is subject to the Open Software License (OSL 3.0) 8 | * that is available through the world-wide-web at this URL: 9 | * http://opensource.org/licenses/osl-3.0.php 10 | * 11 | * @module liqpay 12 | * @category LiqPay 13 | * @package liqpay/liqpay 14 | * @version 3.1 15 | * @author Liqpay 16 | * @copyright Copyright (c) 2014 Liqpay 17 | * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 18 | * 19 | * EXTENSION INFORMATION 20 | * 21 | * LIQPAY API https://www.liqpay.ua/documentation/uk 22 | * 23 | */ 24 | 25 | const axios = require('axios'); 26 | const crypto = require('crypto'); 27 | 28 | /** 29 | * Creates object with helpers for accessing to Liqpay API 30 | * 31 | * @param {string} public_key 32 | * @param {string} private_key 33 | * 34 | * @throws {Error} 35 | */ 36 | module.exports = function LiqPay(public_key, private_key) { 37 | /** 38 | * @member {string} API host 39 | */ 40 | this.host = "https://www.liqpay.ua/api/"; 41 | 42 | this.availableLanguages = ['ru', 'uk', 'en']; 43 | 44 | this.buttonTranslations = {'ru': 'Оплатить', 'uk': 'Сплатити', 'en': 'Pay'}; 45 | /** 46 | * Call API 47 | * 48 | * @param {string} path 49 | * @param {object} params 50 | * @return {object} 51 | * @throws {Error} 52 | */ 53 | this.api = async function api(path, params) { 54 | if (!params.version) { 55 | throw new Error('version is null'); 56 | } 57 | 58 | params.public_key = public_key; 59 | const data = Buffer.from(JSON.stringify(params)).toString('base64'); 60 | const signature = this.str_to_sign(private_key + data + private_key); 61 | 62 | const dataToSend = new URLSearchParams(); 63 | dataToSend.append('data', data); 64 | dataToSend.append('signature', signature); 65 | 66 | try { 67 | const response = await axios.post(this.host + path, dataToSend, { 68 | headers: { 69 | 'Content-Type': 'application/x-www-form-urlencoded' 70 | } 71 | }); 72 | 73 | if (response.status === 200) { 74 | return response.data; 75 | } else { 76 | throw new Error(`Request failed with status code: ${response.status}`); 77 | } 78 | } catch (error) { 79 | throw error; 80 | } 81 | } 82 | 83 | /** 84 | * cnb_form 85 | * 86 | * @param {object} params 87 | * 88 | * @return {string} 89 | * 90 | * @throws {Error} 91 | */ 92 | this.cnb_form = function cnb_form(params) { 93 | let buttonText = this.buttonTranslations['uk']; 94 | if (params.language) { 95 | buttonText = this.buttonTranslations[params.language] || this.buttonTranslations['uk']; 96 | } 97 | 98 | params = this.cnb_params(params); 99 | const data = Buffer.from(JSON.stringify(params)).toString('base64'); 100 | const signature = this.str_to_sign(private_key + data + private_key); 101 | 102 | return '
' + 103 | '' + 104 | '' + 105 | '' + 106 | '' + 107 | '
'; 108 | }; 109 | 110 | /** 111 | * cnb_signature 112 | * 113 | * @param {object} params 114 | * 115 | * @return {string} 116 | * 117 | * @throws {InvalidArgumentException} 118 | */ 119 | this.cnb_signature = function cnb_signature(params) { 120 | params = this.cnb_params(params); 121 | const data = Buffer.from(JSON.stringify(params)).toString('base64'); 122 | return this.str_to_sign(private_key + data + private_key); 123 | }; 124 | 125 | /** 126 | * cnb_params 127 | * 128 | * @param {object} params 129 | * 130 | * @return {object} params 131 | * 132 | * @throws {Error} 133 | */ 134 | this.cnb_params = function cnb_params(params) { 135 | params.public_key = public_key; 136 | 137 | // Validate and convert version to number 138 | if (params.version) { 139 | if (typeof params.version === 'string' && !isNaN(Number(params.version))) { 140 | params.version = Number(params.version); 141 | } else if (typeof params.version !== 'number') { 142 | throw new Error('version must be a number or a string that can be converted to a number'); 143 | } 144 | } else { 145 | throw new Error('version is null'); 146 | } 147 | 148 | // Validate and convert amount to number 149 | if (params.amount) { 150 | if (typeof params.amount === 'string' && !isNaN(Number(params.amount))) { 151 | params.amount = Number(params.amount); 152 | } else if (typeof params.amount !== 'number') { 153 | throw new Error('amount must be a number or a string that can be converted to a number'); 154 | } 155 | } else { 156 | throw new Error('amount is null'); 157 | } 158 | 159 | // Ensure other parameters are strings 160 | const stringParams = ['action', 'currency', 'description', 'language']; 161 | for (const param of stringParams) { 162 | if (params[param] && typeof params[param] !== 'string') { 163 | params[param] = String(params[param]); 164 | } else if (!params[param] && param !== 'language') { // language is optional 165 | throw new Error(`${param} is null or not provided`); 166 | } 167 | } 168 | 169 | // Check if language is set and is valid 170 | if (params.language && !this.availableLanguages.includes(params.language)) { 171 | params.language= 'uk'; 172 | } 173 | 174 | return params; 175 | }; 176 | 177 | 178 | /** 179 | * str_to_sign 180 | * 181 | * @param {string} str 182 | * 183 | * @return {string} 184 | * 185 | * @throws {Error} 186 | */ 187 | this.str_to_sign = function str_to_sign(str) { 188 | if (typeof str !== 'string') { 189 | throw new Error('Input must be a string'); 190 | } 191 | 192 | const sha1 = crypto.createHash('sha1'); 193 | sha1.update(str); 194 | return sha1.digest('base64'); 195 | }; 196 | 197 | /** 198 | * Return Form Object 199 | * 200 | * @param {object} params 201 | * 202 | * @returns {{ data: string, signature: string }} Form Object 203 | */ 204 | this.cnb_object = function cnb_object(params) { 205 | params.language = params.language || "uk"; 206 | params = this.cnb_params(params); 207 | const data = Buffer.from(JSON.stringify(params)).toString('base64'); 208 | const signature = this.str_to_sign(private_key + data + private_key); 209 | return {data: data, signature: signature}; 210 | }; 211 | 212 | return this; 213 | }; 214 | -------------------------------------------------------------------------------- /test/liqpay.test.js: -------------------------------------------------------------------------------- 1 | const LiqPay = require('../lib/liqpay'); // Adjust the path to your LiqPay class file 2 | const crypto = require('crypto'); 3 | const axios = require('axios'); 4 | 5 | jest.mock('axios'); // This will mock the axios module 6 | describe('LiqPay class', () => { 7 | 8 | let liqPayInstance; 9 | 10 | beforeEach(() => { 11 | liqPayInstance = new LiqPay('public key', 'Private key'); // Assuming you have a constructor in your LiqPay class 12 | }); 13 | 14 | describe('cnb_form method', () => { 15 | 16 | let liqPayInstance; 17 | 18 | beforeEach(() => { 19 | liqPayInstance = new LiqPay('public key', 'Private key'); // Assuming you have a constructor in your LiqPay class 20 | }); 21 | 22 | it('should return a form with correct data and signature', () => { 23 | const params = { 24 | action: 'pay', 25 | amount: '100', 26 | currency: 'USD', 27 | description: 'Test payment', 28 | order_id: 'order12345', 29 | version: '3', 30 | language: 'ru' 31 | }; 32 | 33 | const form = liqPayInstance.cnb_form(params); 34 | 35 | // Check if form contains the correct data and signature 36 | expect(form).toContain('name="data"'); 37 | expect(form).toContain('name="signature"'); 38 | 39 | // Check if the button label is set correctly 40 | expect(form).toContain(' { 44 | const params = { 45 | action: 'pay', 46 | amount: '100', 47 | currency: 'USD', 48 | description: 'Test payment', 49 | order_id: 'order12345', 50 | version: '3' 51 | }; 52 | 53 | const form = liqPayInstance.cnb_form(params); 54 | 55 | // Check if the button label is set to Ukrainian by default 56 | expect(form).toContain(' { 60 | const paramsWithLanguage = { 61 | version: 3, 62 | action: 'pay', 63 | amount: 100.5, 64 | currency: 'USD', 65 | description: 'Test payment', 66 | order_id: 'order12345', 67 | language: 'ru' // Assuming 'ru' is a valid key in buttonTranslations 68 | }; 69 | 70 | const form = liqPayInstance.cnb_form(paramsWithLanguage); 71 | expect(form).toContain(' { 77 | 78 | let liqPayInstance; 79 | 80 | beforeEach(() => { 81 | liqPayInstance = new LiqPay('public key', 'Private key'); // Assuming you have a constructor in your LiqPay class 82 | }); 83 | 84 | it('should convert version and amount to numbers if they are valid strings', () => { 85 | const params = { 86 | version: '3', 87 | action: 'pay', 88 | amount: '100.5', 89 | currency: 'USD', 90 | description: 'Test payment', 91 | order_id: 'order12345' 92 | }; 93 | 94 | const result = liqPayInstance.cnb_params(params); 95 | 96 | expect(typeof result.version).toBe('number'); 97 | expect(typeof result.amount).toBe('number'); 98 | }); 99 | 100 | it('should throw an error if version or amount are invalid strings', () => { 101 | const params = { 102 | version: 'invalid', 103 | action: 'pay', 104 | amount: '100.5', 105 | currency: 'USD', 106 | description: 'Test payment', 107 | order_id: 'order12345' 108 | }; 109 | 110 | expect(() => liqPayInstance.cnb_params(params)).toThrow('version must be a number or a string that can be converted to a number'); 111 | }); 112 | 113 | it('should convert other parameters to strings if they are not already strings', () => { 114 | const params = { 115 | version: 3, 116 | action: 'pay', 117 | amount: 100.5, 118 | currency: 123, 119 | description: true, 120 | order_id: 'order12345' 121 | }; 122 | 123 | const result = liqPayInstance.cnb_params(params); 124 | 125 | expect(typeof result.currency).toBe('string'); 126 | expect(typeof result.description).toBe('string'); 127 | }); 128 | 129 | it('should throw an error if a required parameter is missing', () => { 130 | const params = { 131 | version: 3, 132 | action: 'pay', 133 | amount: 100.5, 134 | currency: 'USD', 135 | // description is missing 136 | order_id: 'order12345' 137 | }; 138 | 139 | expect(() => liqPayInstance.cnb_params(params)).toThrow('description is null or not provided'); 140 | }); 141 | it('should throw an error if version is missing', () => { 142 | const paramsWithoutVersion = { 143 | action: 'pay', 144 | amount: '100.5', 145 | currency: 'USD', 146 | description: 'Test payment', 147 | order_id: 'order12345' 148 | }; 149 | 150 | expect(() => liqPayInstance.cnb_params(paramsWithoutVersion)).toThrow('version is null'); 151 | }); 152 | 153 | it('should throw an error if amount is an invalid string', () => { 154 | const paramsWithInvalidAmount = { 155 | version: 3, 156 | action: 'pay', 157 | amount: 'invalidAmount', 158 | currency: 'USD', 159 | description: 'Test payment', 160 | order_id: 'order12345' 161 | }; 162 | 163 | expect(() => liqPayInstance.cnb_params(paramsWithInvalidAmount)).toThrow('amount must be a number or a string that can be converted to a number'); 164 | }); 165 | 166 | it('should throw an error if amount is missing', () => { 167 | const paramsWithoutAmount = { 168 | version: 3, 169 | action: 'pay', 170 | currency: 'USD', 171 | description: 'Test payment', 172 | order_id: 'order12345' 173 | }; 174 | 175 | expect(() => liqPayInstance.cnb_params(paramsWithoutAmount)).toThrow('amount is null'); 176 | }); 177 | 178 | }); 179 | 180 | describe('str_to_sign function', () => { 181 | 182 | let liqPayInstance; 183 | 184 | beforeEach(() => { 185 | liqPayInstance = new LiqPay('public key', 'Private key'); // Assuming you have a constructor in your LiqPay class 186 | }); 187 | 188 | it('should return a base64 encoded SHA-1 hash of the input string', () => { 189 | const input = "test"; 190 | const output = liqPayInstance.str_to_sign(input); 191 | const expectedOutput = crypto.createHash('sha1').update(input).digest('base64'); 192 | 193 | expect(output).toBe(expectedOutput); 194 | }); 195 | 196 | it('should throw an error if the input is not a string', () => { 197 | const input = 12345; // a number, not a string 198 | expect(() => liqPayInstance.str_to_sign(input)).toThrow('Input must be a string'); 199 | }); 200 | 201 | it('should throw an error if the input is null', () => { 202 | const input = null; 203 | expect(() => liqPayInstance.str_to_sign(input)).toThrow('Input must be a string'); 204 | }); 205 | 206 | it('should throw an error if the input is undefined', () => { 207 | const input = undefined; 208 | expect(() => liqPayInstance.str_to_sign(input)).toThrow('Input must be a string'); 209 | }); 210 | }); 211 | 212 | describe('cnb_object function', () => { 213 | 214 | 215 | it('should return an object with data and signature properties', () => { 216 | const params = { 217 | version: 3, 218 | action: 'pay', 219 | amount: 100.5, 220 | currency: 'USD', 221 | description: 'Test payment', 222 | order_id: 'order12345', 223 | language: 'en' 224 | }; 225 | const result = liqPayInstance.cnb_object(params); 226 | 227 | expect(result).toHaveProperty('data'); 228 | expect(result).toHaveProperty('signature'); 229 | }); 230 | 231 | // Add more tests as needed 232 | }); 233 | 234 | describe('cnb_signature function', () => { 235 | it('should return a valid signature for given params', () => { 236 | const params = { 237 | version: 3, 238 | action: 'pay', 239 | amount: 100.5, 240 | currency: 'USD', 241 | description: 'Test payment', 242 | order_id: 'order12345' 243 | }; 244 | 245 | const signature = liqPayInstance.cnb_signature(params); 246 | 247 | // Here, we're replicating the signature generation process to validate the result 248 | const data = Buffer.from(JSON.stringify(liqPayInstance.cnb_params(params))).toString('base64'); 249 | const expectedSignature = liqPayInstance.str_to_sign('Private key' + data + 'Private key'); // Replace 'private key' with the actual private key if it's different 250 | 251 | expect(signature).toBe(expectedSignature); 252 | }); 253 | 254 | it('should throw an error if a required parameter is missing', () => { 255 | const params = { 256 | version: 3, 257 | action: 'pay', 258 | amount: 100.5, 259 | currency: 'USD', 260 | // description is missing 261 | order_id: 'order12345' 262 | }; 263 | 264 | expect(() => liqPayInstance.cnb_signature(params)).toThrow('description is null or not provided'); 265 | }); 266 | }); 267 | 268 | describe('api function', () => { 269 | it('should return data when the request is successful', async () => { 270 | const mockData = { success: true }; 271 | axios.post.mockResolvedValue({ status: 200, data: mockData }); 272 | 273 | const params = { 274 | version: 3, 275 | action: 'pay', 276 | amount: 100.5, 277 | currency: 'USD', 278 | description: 'Test payment', 279 | order_id: 'order12345' 280 | }; 281 | 282 | const result = await liqPayInstance.api('/test-path', params); 283 | expect(result).toEqual(mockData); 284 | }); 285 | 286 | it('should throw an error when the request fails', async () => { 287 | axios.post.mockResolvedValue({ status: 400 }); 288 | 289 | const params = { 290 | version: 3, 291 | action: 'pay', 292 | amount: 100.5, 293 | currency: 'USD', 294 | description: 'Test payment', 295 | order_id: 'order12345' 296 | }; 297 | 298 | await expect(liqPayInstance.api('/test-path', params)).rejects.toThrow('Request failed with status code: 400'); 299 | }); 300 | 301 | it('should throw an error if version is missing', async () => { 302 | const paramsWithoutVersion = { 303 | action: 'pay', 304 | amount: 100.5, 305 | currency: 'USD', 306 | description: 'Test payment', 307 | order_id: 'order12345' 308 | }; 309 | 310 | await expect(liqPayInstance.api('/test-path', paramsWithoutVersion)).rejects.toThrow('version is null'); 311 | }); 312 | }); 313 | 314 | }); 315 | --------------------------------------------------------------------------------