├── .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 |
3 |
:package: cmi-payment-nodejs
4 | npm package to communicate with the CMI payment plateform in Morocco
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Report Bug
24 | Request Feature
25 |
26 |
27 | [](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 | 
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 += "';
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 |
--------------------------------------------------------------------------------