├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml ├── images │ ├── cmi.png │ └── screenshot.png └── workflows │ ├── coverage.yml │ ├── delete-workflow-runs.yml │ └── npm-publish.yml ├── .gitignore ├── .gitpod.yml ├── .prettierrc ├── LICENSE.md ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── __tests__ │ └── index.test.ts ├── classes │ ├── BaseCmiClient.ts │ └── CmiClient.ts ├── helpers │ └── validator.ts ├── index.ts ├── interfaces │ └── CmiClientInterface.ts └── types │ └── index.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm ci 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | 16 | jobs: 17 | build: 18 | working_directory: ~/nest 19 | docker: 20 | - image: circleci/node:16 21 | steps: 22 | - checkout 23 | - run: 24 | name: Update NPM version 25 | command: "sudo npm install -g npm@latest" 26 | - restore_cache: 27 | key: dependency-cache-{{ checksum "package.json" }} 28 | - run: 29 | name: Install dependencies 30 | command: npm ci 31 | - save_cache: 32 | key: dependency-cache-{{ checksum "package.json" }} 33 | paths: 34 | - ./node_modules 35 | - run: 36 | name: Build 37 | command: npm run build 38 | 39 | workflows: 40 | version: 2 41 | build-and-test: 42 | jobs: 43 | - build 44 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | 4 | # don't lint build output (make sure it's set to your correct build folder name) 5 | lib 6 | 7 | # don't lint nyc coverage output 8 | coverage 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "jest"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:jest/recommended" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: "develop" 8 | labels: 9 | - "dependencies" 10 | -------------------------------------------------------------------------------- /.github/images/cmi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aitmiloud/cmi-node/8f7a0a46722fb03a54245770e083e38eb5ac0d48/.github/images/cmi.png -------------------------------------------------------------------------------- /.github/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aitmiloud/cmi-node/8f7a0a46722fb03a54245770e083e38eb5ac0d48/.github/images/screenshot.png -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Running Code Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 2 19 | 20 | - name: Set up Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Run the tests 29 | run: npm test -- --coverage 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v2 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v2 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/delete-workflow-runs.yml: -------------------------------------------------------------------------------- 1 | name: Delete old workflow runs 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | days: 6 | description: "Number of days." 7 | required: true 8 | default: 0 9 | minimum_runs: 10 | description: "The minimum runs to keep for each workflow." 11 | required: true 12 | default: 0 13 | delete_workflow_pattern: 14 | description: "The name or filename of the workflow. if not set then it will target all workflows." 15 | required: false 16 | delete_workflow_by_state_pattern: 17 | description: "Remove workflow by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually" 18 | required: true 19 | default: "All" 20 | type: choice 21 | options: 22 | - "All" 23 | - active 24 | - deleted 25 | - disabled_inactivity 26 | - disabled_manually 27 | dry_run: 28 | description: "Only log actions, do not perform any delete operations." 29 | required: false 30 | 31 | jobs: 32 | del_runs: 33 | runs-on: windows-latest 34 | steps: 35 | - name: Delete workflow runs 36 | uses: Mattraks/delete-workflow-runs@v2 37 | with: 38 | token: ${{ github.token }} 39 | repository: ${{ github.repository }} 40 | retain_days: ${{ github.event.inputs.days }} 41 | keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} 42 | delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }} 43 | delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }} 44 | dry_run: ${{ github.event.inputs.dry_run }} 45 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js build and publish package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - run: npm i 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm i 28 | - run: npm run build 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # npm library files 38 | lib/ 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | out 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | .yarn/cache 116 | .yarn/unplugged 117 | .yarn/build-state.yml 118 | .yarn/install-state.gz 119 | .pnp.* 120 | 121 | .idea/ 122 | 123 | # Testing Html page 124 | test.html 125 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install && npm run build 3 | 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Aitmiloud Mohamed 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | :package: cmi-payment-nodejs 3 |

:package: cmi-payment-nodejs

4 |

npm package to communicate with the CMI payment plateform in Morocco

5 |

6 | 7 | Issues 8 | 9 | 10 | GitHub pull requests 11 | 12 | 13 | Npm Total Downloads 14 | 15 | 16 | GitHub release 17 | 18 | 19 | GitHub Downloads 20 | 21 |
22 |
23 | Report Bug 24 | Request Feature 25 |

26 | 27 | [![codecov](https://codecov.io/gh/aitmiloud/cmi-node/branch/main/graph/badge.svg?token=Q9fr548J0D)](https://codecov.io/gh/aitmiloud/cmi-node) 28 | 29 | ## Installation 30 | 31 | Install cmi-node with npm 32 | 33 | ```bash 34 | npm i cmi-payment-nodejs 35 | ``` 36 | 37 | ## Usage/Examples using Express.js 38 | 39 | ```javascript 40 | const express = require('express'); 41 | const cmi = require('cmi-payment-nodejs'); 42 | 43 | const app = express(); 44 | const port = 3000; 45 | 46 | // Define a route to initiate a CMI payment 47 | app.get('/pay', (req, res) => { 48 | // Extract necessary information from the request query parameters 49 | const { amount, email, tel, BillToName } = req.query; 50 | 51 | // Initialize the CMI payment client with your configuration 52 | const CmiClient = new cmi.default({ 53 | storekey: 'YOUR_STOREKEY', // Your CMI Store Key 54 | clientid: 'YOUR_CLIENTID', // Your CMI Client ID 55 | oid: 'UNIQUE_COMMAND_ID', // A unique command ID 56 | shopurl: 'https://your-shop-url.com', // Your shop's URL for redirection 57 | okUrl: 'https://your-success-redirect-url.com', // Redirection after a successful payment 58 | failUrl: 'https://your-failure-redirect-url.com', // Redirection after a failed payment 59 | email, // email for CMI platform 60 | BillToName, // name as it should appear on the CMI platform 61 | amount, // The amount to be paid 62 | callbackURL: 'https://your-callback-url.com', // Callback URL for payment confirmation 63 | tel, // phone number for the CMI platform 64 | }); 65 | 66 | // Generate an HTML form for the CMI payment 67 | const htmlForm = CmiClient.redirect_post(); 68 | 69 | // Send the HTML form as the response 70 | res.send(htmlForm); 71 | }); 72 | 73 | app.listen(port, () => { 74 | console.log(`App is listening on port ${port}`); 75 | }); 76 | 77 | ``` 78 | 79 | 80 | ## 81 | 82 | ![App Screenshot](.github/images/screenshot.png) 83 | 84 | 85 | ## Basic test card numbers 86 | Credit Card information cannot be used in test mode. instead, use any of the following test card numbers, a valid expiration date in the future, and any random CVC number, to create a successful payment. 87 | 88 | Branch : `visa`, PAN: `4000000000000010`, Expired date: `make any date` CVC: `000` 89 | 90 | Branch : `MasterCard`, PAN: `5453010000066100`, Expired date: `make any date` CVC: `000` 91 | 92 | ## 3D Secure test card numbers 93 | The following card information try to tests local payments such as Strong Customer Authentication **SCA** 94 | 95 | Branch : `MasterCard`, PAN: `5191630100004896`, Authentication code: `123` Expired date: `make any date` CVC: `000` 96 | 97 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(t|j)s$': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(t|j)s$', 6 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmi-payment-nodejs", 3 | "version": "1.1.4", 4 | "description": "npm package to communicate with the CMI payment plateform in Morocco", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "engines": { 8 | "node": ">=8.0.0 <22.0.0" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "format": "prettier --write \"src/**/*.(js|ts)\"", 13 | "lint": "eslint src --ext .js,.ts", 14 | "lint:fix": "eslint src --fix --ext .js,.ts", 15 | "test": "jest --config jest.config.js", 16 | "prepare": "npm run build", 17 | "prepublishOnly": "npm test && npm run lint", 18 | "preversion": "npm run lint", 19 | "version": "npm run format && git add -A src", 20 | "postversion": "git push && git push --tags" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/aitmiloud/cmi-node" 25 | }, 26 | "keywords": [ 27 | "cmi", 28 | "cmi nodejs package", 29 | "cmi npm package", 30 | "cmi online payment", 31 | "payment processing", 32 | "morocco payment", 33 | "cmi payment", 34 | "payment in morocco", 35 | "paiement en ligne cmi", 36 | "cmi paiement en ligne", 37 | "paiement en ligne maroc", 38 | "cmi paiement node", 39 | "cmi payment node" 40 | ], 41 | "author": { 42 | "name": "Aitmiloud Mohamed", 43 | "email": "aitmiloud@atzapps.com", 44 | "url": "https://atzapps.com" 45 | }, 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/aitmiloud/cmi-node/issues" 49 | }, 50 | "homepage": "https://github.com/aitmiloud/cmi-node#readme", 51 | "devDependencies": { 52 | "@types/jest": "29.4.0", 53 | "@typescript-eslint/eslint-plugin": "5.54.0", 54 | "@typescript-eslint/parser": "5.52.0", 55 | "eslint": "8.35.0", 56 | "eslint-plugin-jest": "27.2.1", 57 | "jest": "29.4.3", 58 | "prettier": "2.8.4", 59 | "ts-jest": "29.0.5", 60 | "typescript": "4.9.5" 61 | }, 62 | "files": [ 63 | "lib/**/*" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import CmiClient from '../index'; 2 | 3 | const validOptions = { 4 | storekey: 'Atzapps_store_key23', 5 | clientid: '60000999', 6 | oid: '135ABC', 7 | shopurl: 'https://google.com', 8 | okUrl: 'https://google.com', 9 | failUrl: 'https://google.com', 10 | email: 'test@gmail.com', 11 | BillToName: 'test', 12 | amount: '7896', 13 | callbackURL: 'https://google.com', 14 | tel: '212652124874', 15 | }; 16 | 17 | describe('CmiClient', () => { 18 | let cmiClient: CmiClient; 19 | 20 | beforeEach(() => { 21 | cmiClient = new CmiClient(validOptions); 22 | }); 23 | 24 | test('creates a new instance of CmiClient', () => { 25 | expect(cmiClient).toBeInstanceOf(CmiClient); 26 | }); 27 | 28 | test('generates a hash string', () => { 29 | const hash = cmiClient.generateHash(validOptions.storekey); 30 | expect(typeof hash).toBe('string'); 31 | }); 32 | 33 | test('returns default options object', () => { 34 | const defaultOpts = cmiClient.getDefaultOpts(); 35 | expect(defaultOpts).toBeInstanceOf(Object); 36 | }); 37 | 38 | test('returns required options object', () => { 39 | const requiredOpts = cmiClient.getRequireOpts(); 40 | expect(requiredOpts).toBeInstanceOf(Object); 41 | }); 42 | 43 | describe('invalid options', () => { 44 | const invalidCases = [ 45 | { key: 'storekey', message: 'storekey is required' }, 46 | { key: 'clientid', message: 'clientid is required' }, 47 | { key: 'oid', message: 'oid is required' }, 48 | { key: 'okUrl', message: 'okUrl is required' }, 49 | { key: 'failUrl', message: 'failUrl is required' }, 50 | { key: 'email', message: 'email is required' }, 51 | { key: 'BillToName', message: 'BillToName is required' }, 52 | { key: 'amount', message: 'amount is required' }, 53 | { key: 'callbackURL', message: 'callbackURL is required' }, 54 | ]; 55 | 56 | invalidCases.forEach(({ key, message }) => { 57 | test(`throws an error if ${key} is not provided`, () => { 58 | const invalidOptions = { ...validOptions, [key]: null }; 59 | expect(() => new CmiClient(invalidOptions)).toThrow(message); 60 | }); 61 | }); 62 | 63 | test('throws an error if email is not a valid email', () => { 64 | const invalidOptions = { ...validOptions, email: 'invalid_email' }; 65 | expect(() => new CmiClient(invalidOptions)).toThrow('email must be a valid email'); 66 | }); 67 | 68 | test('throws an error if lang is not one of "ar", "fr", "en"', () => { 69 | const invalidOptions = { ...validOptions, lang: 'es' }; 70 | expect(() => new CmiClient(invalidOptions)).toThrow('lang must be one of these languages: ar, fr, en'); 71 | }); 72 | }); 73 | 74 | test('returns an HTML form string', () => { 75 | const form = cmiClient.redirect_post(); 76 | expect(typeof form).toBe('string'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/classes/BaseCmiClient.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto'; 2 | import { CmiClientinterface } from '../interfaces/CmiClientInterface'; 3 | import { CmiOptions, ValidationRule } from '../types'; 4 | 5 | export default class BaseCmiClient implements CmiClientinterface { 6 | /** 7 | * string default base url for CMI's API 8 | */ 9 | readonly DEFAULT_API_BASE_URL = 'https://testpayment.cmi.co.ma'; 10 | 11 | /** 12 | * array of languages supported by CMI 13 | */ 14 | readonly LANGUAGES = ['ar', 'fr', 'en']; 15 | 16 | /** 17 | * array of required options for CMI 18 | */ 19 | requiredOpts: CmiOptions; 20 | 21 | /** 22 | * Initializes a new instance of the {CmiClient} class. 23 | * 24 | * The constructor takes a require multiple argument. it must be an array 25 | * 26 | * Configuration setting include the following options: 27 | * 28 | * - storekey (string) : it's necessary to generate hash key 29 | * - clientid (string) : it given by CMI you should contcat them to get a unique clientid 30 | * - oid (string) : command_id it should be unique for each time your would like to make transaction 31 | * - okUrl (string) The URL used to redirect the customer back to the mechant's web site (accepted payment) 32 | * - failUrl (string) The URL used to redirect the customer back to the mechant's web site (failed/rejected payment) 33 | * - email (string) Customer email 34 | * - BillToName (string) Customer's name (firstname and lastname) 35 | * - amount (Numeric) Transaction amount 36 | */ 37 | constructor(requiredOpts: CmiOptions) { 38 | if (!requiredOpts) { 39 | throw new Error('requiredOpts is required'); 40 | } 41 | 42 | // MERGE DEFAULT OPTIONS WITH REQUIRED OPTIONS AND ASSIGN IT TO REQUIRED OPTIONS 43 | requiredOpts = { ...this.getDefaultOpts(), ...requiredOpts }; 44 | 45 | // VALIDATE REQUIRED OPTIONS 46 | this.validateOptions(requiredOpts); 47 | 48 | // ASSIGN 49 | this.requiredOpts = requiredOpts; 50 | } 51 | 52 | /** 53 | * Get default cmi options 54 | * @returns {CmiOptions} default cmi options 55 | */ 56 | getDefaultOpts(): CmiOptions { 57 | return { 58 | storetype: '3D_PAY_HOSTING', 59 | TranType: 'PreAuth', 60 | currency: '504', // 504 is MAD 61 | rnd: Date.now().toString(), 62 | lang: 'fr', 63 | hashAlgorithm: 'ver3', 64 | encoding: 'UTF-8', // Optional 65 | refreshtime: '5', // Optional 66 | }; 67 | } 68 | 69 | /** 70 | * Get all required options 71 | * @returns {CmiOptions} required options 72 | */ 73 | public getRequireOpts(): CmiOptions { 74 | return this.requiredOpts; 75 | } 76 | 77 | /** 78 | * Generate Hash to make request to CMI page 79 | * @returns {string} calculated hash 80 | */ 81 | public generateHash(storekey: string | null | undefined = ''): string { 82 | // amount|BillToCompany|BillToName|callbackUrl|clientid|currency|email|failUrl|hashAlgorithm|lang|okurl|rnd|storetype|TranType|storeKey 83 | /** 84 | * ASSIGNE STORE KEY 85 | */ 86 | if (storekey == null || storekey == undefined || storekey == '') { 87 | storekey = this.requiredOpts.storekey; 88 | } 89 | 90 | // EXCLUDE STOREKEY FROM REQUIRE OPTIONS 91 | delete this.requiredOpts.storekey; 92 | 93 | const cmiParams = this.requiredOpts; 94 | // sort the required options by key alphabetically like natcasesort in php 95 | const sortedKeys = Object.keys(cmiParams).sort((a, b) => 96 | a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }), 97 | ); 98 | const sortedCmiParams = {}; 99 | type T = keyof typeof sortedCmiParams; 100 | sortedKeys.forEach((key) => { 101 | sortedCmiParams[key as T] = cmiParams[key as T]; 102 | }); 103 | 104 | let hashval = ''; 105 | for (const key in sortedCmiParams) { 106 | if (key != 'HASH' && key != 'encoding') { 107 | hashval += sortedCmiParams[key as T] + '|'; 108 | } 109 | } 110 | 111 | hashval += storekey; 112 | 113 | const hash = crypto.createHash('sha512').update(hashval).digest('hex'); 114 | // convert it to base64 115 | const calculatedHash = Buffer.from(hash, 'hex').toString('base64'); 116 | this.requiredOpts.HASH = calculatedHash; 117 | return calculatedHash; 118 | } 119 | 120 | private validateOptions(opts: CmiOptions): void { 121 | const validationRules: ValidationRule[] = [ 122 | { field: 'storekey', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 123 | { field: 'clientid', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 124 | { field: 'storetype', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 125 | { field: 'TranType', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 126 | { field: 'amount', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 127 | { field: 'currency', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 128 | { field: 'oid', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true }, 129 | { field: 'okUrl', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true, isURL: true }, 130 | { field: 'failUrl', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true, isURL: true }, 131 | { 132 | field: 'lang', 133 | required: true, 134 | type: 'stringOrNull', 135 | allowEmpty: false, 136 | noWhitespace: true, 137 | validLang: ['ar', 'fr', 'en'], 138 | }, 139 | { field: 'email', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: true, isEmail: true }, 140 | { field: 'BillToName', required: true, type: 'stringOrNull', allowEmpty: false, noWhitespace: false }, 141 | { 142 | field: 'hashAlgorithm', 143 | required: true, 144 | type: 'stringOrNull', 145 | allowEmpty: false, 146 | noWhitespace: true, 147 | validHashAlgorithm: ['SHA1', 'SHA256', 'SHA512'], 148 | }, 149 | { 150 | field: 'callbackURL', 151 | required: true, 152 | type: 'stringOrNull', 153 | allowEmpty: false, 154 | noWhitespace: true, 155 | isURL: true, 156 | }, 157 | ]; 158 | 159 | for (const rule of validationRules) { 160 | const value = opts[rule.field]; 161 | 162 | if (rule.required && (value === undefined || value === null)) { 163 | throw new Error(`${rule.field} is required`); 164 | } 165 | 166 | if (value !== null) { 167 | if (rule.type === 'stringOrNull' && typeof value !== 'string' && value !== null) { 168 | throw new Error(`${rule.field} must be a string or null`); 169 | } 170 | 171 | if (rule.isURL && typeof value === 'string' && !/^https?:\/\/.+/.test(value)) { 172 | throw new Error(`${rule.field} must be a valid URL`); 173 | } 174 | 175 | if (rule.isEmail && typeof value === 'string' && !/^.+@.+\..+$/.test(value)) { 176 | throw new Error(`${rule.field} must be a valid email`); 177 | } 178 | 179 | if (rule.validLang && typeof value === 'string' && !rule.validLang.includes(value)) { 180 | throw new Error(`${rule.field} must be one of these languages: ${rule.validLang.join(', ')}`); 181 | } 182 | 183 | if (!rule.allowEmpty && (value === '' || (typeof value === 'string' && /^\s*$/.test(value)))) { 184 | throw new Error(`${rule.field} can't be empty`); 185 | } 186 | 187 | if (rule.noWhitespace && typeof value === 'string' && /\s/.test(value)) { 188 | throw new Error(`${rule.field} can't contain whitespace`); 189 | } 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/classes/CmiClient.ts: -------------------------------------------------------------------------------- 1 | import BaseCmiClient from './BaseCmiClient'; 2 | 3 | export class CmiClient extends BaseCmiClient { 4 | public redirect_post(): string { 5 | /** 6 | * GENERATE HASH 7 | */ 8 | this.generateHash(); 9 | 10 | /** 11 | * HANDLE REQUIRE OPTIONS HIDDEN INPUTS AND REDIRECT TO CMI PAGE 12 | * CREATE INPUTS HIDDEN, GENERATE A VALID HASH AND MAKE REDIRECT POST TO CMI 13 | */ 14 | const url = this.DEFAULT_API_BASE_URL + '/fim/est3Dgate'; 15 | 16 | let html = ''; 17 | html += ''; 18 | html += ""; 19 | html += ""; 20 | html += ""; 21 | html += ""; 22 | html += ''; 23 | html += ""; 24 | html += "
"; 25 | 26 | for (const [name, value] of Object.entries(this.getRequireOpts())) { 27 | html += " "; 28 | } 29 | 30 | html += '
'; 31 | html += "'; 34 | html += ''; 35 | return html; 36 | } 37 | 38 | /** 39 | * Check status hash from CMI plateform if is equal to hash generated 40 | * 41 | * @param HASH 42 | * @return bool 43 | */ 44 | 45 | public checkHash(hash: string): boolean { 46 | return this.generateHash() === hash; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/helpers/validator.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aitmiloud/cmi-node/8f7a0a46722fb03a54245770e083e38eb5ac0d48/src/helpers/validator.ts -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CmiClient } from './classes/CmiClient'; 2 | 3 | export default CmiClient; 4 | -------------------------------------------------------------------------------- /src/interfaces/CmiClientInterface.ts: -------------------------------------------------------------------------------- 1 | import type { CmiOptions } from '../types'; 2 | 3 | export interface CmiClientinterface { 4 | getDefaultOpts(): CmiOptions; 5 | 6 | getRequireOpts(): CmiOptions; 7 | 8 | generateHash(storekey: null): string; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface CmiOptions { 2 | storekey?: string; 3 | clientid?: string; 4 | okUrl?: string; 5 | failUrl?: string; 6 | shopurl?: string; 7 | storetype?: string; 8 | TranType?: string; 9 | currency?: string; 10 | rnd?: string; 11 | amount?: string; 12 | lang?: string; 13 | hashAlgorithm?: string; 14 | encoding?: string; 15 | refreshtime?: string; 16 | callbackURL?: string; 17 | oid?: string; 18 | email?: string; 19 | BillToName?: string; 20 | HASH?: string; 21 | } 22 | 23 | export type ValidationRule = { 24 | field: keyof CmiOptions; 25 | required: boolean; 26 | type: 'stringOrNull' | 'isURL' | 'validLang' | 'isEmail' | 'validHashAlgorithm'; 27 | allowEmpty: boolean; 28 | noWhitespace: boolean; 29 | validLang?: string[]; 30 | validHashAlgorithm?: string[]; 31 | isURL?: boolean; 32 | isEmail?: boolean; 33 | }; 34 | 35 | // export interface CmiHashParams {} 36 | 37 | // export interface CmiGenerateLinkOptions {} 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./lib" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["src"], 70 | "exclude": ["node_modules", "**/__tests__/*"] 71 | } 72 | --------------------------------------------------------------------------------