diff --git a/.commitlintrc.js b/.commitlintrc.js
new file mode 100644
index 0000000..4da806b
--- /dev/null
+++ b/.commitlintrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+ scopes: [{ name: 'client' }, { name: 'tutorial' }],
+ scopeOverrides: {
+ fix: [{ name: 'style' }, { name: 'unit' }, { name: 'e2e' }, { name: 'integration' }],
+ },
+ allowCustomScopes: true,
+};
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c26529e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+max_line_length = 160
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml, yaml}]
+indent_size = 2
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3541036
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+##################################################
+# These variables are used for integration tests #
+##################################################
+SQUARE_BASE_URL = https://connect.squareup.com
+SQUARE_SANDBOX_BASE_URL = https://connect.squareupsandbox.com
+SQUARE_ACCESS_TOKEN =
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..724588f
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,94 @@
+/**
+ * How to install and use
+ * @link {https://www.arden.nl/setting-up-a-gatsby-js-starter-with-type-script-es-lint-prettier-and-pre-commit-hooks}
+ *
+ * Rule documentation
+ * @link {https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md}
+ */
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ node: true,
+ es6: true,
+ mocha: true,
+ },
+ parser: '@typescript-eslint/parser',
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
+ 'plugin:prettier/recommended',
+ 'plugin:import/errors',
+ 'plugin:import/warnings',
+ 'plugin:import/typescript',
+ 'prettier',
+ 'prettier/@typescript-eslint',
+ ],
+ settings: {
+ 'import/parsers': {
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
+ },
+ },
+ parserOptions: {
+ project: './tsconfig.eslint.json',
+ },
+ plugins: ['@typescript-eslint', 'import', 'prettier'],
+ rules: {
+ 'prettier/prettier': 'error',
+ 'import/no-deprecated': ['error'],
+ 'import/order': ['error', { groups: ['builtin', 'external', 'parent', 'sibling', 'index'] }],
+ '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
+ '@typescript-eslint/explicit-function-return-type': ['error'],
+ '@typescript-eslint/interface-name-prefix': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-inferrable-types': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-empty-interface': 'off',
+ '@typescript-eslint/no-require-imports': ['error'],
+ '@typescript-eslint/no-use-before-define': ['error'],
+ '@typescript-eslint/camelcase': 'off',
+ '@typescript-eslint/ban-ts-ignore': 'off',
+ '@typescript-eslint/require-await': 'off',
+ '@typescript-eslint/promise-function-async': [
+ 'error',
+ {
+ allowAny: true,
+ },
+ ],
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
+ types: {
+ Number: {
+ message: 'Use number instead',
+ fixWith: 'number',
+ },
+ Function: {
+ message: 'Use () => void instead',
+ fixWith: '() => void',
+ },
+ Object: {
+ message: 'Use object instead',
+ fixWith: 'object',
+ },
+ String: {
+ message: 'Use string instead',
+ fixWith: 'string',
+ },
+ },
+ },
+ ],
+ },
+ overrides: [
+ {
+ files: ['*.js', '*.jsx'],
+ rules: {
+ '@typescript-eslint/no-require-imports': 'off',
+ '@typescript-eslint/no-var-requires': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ },
+ },
+ ],
+};
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..988cdb9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+* text=auto
+* text eol=lf
+*.png binary
+*.gif binary
diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md
new file mode 100644
index 0000000..bff59a2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-bug-report.md
@@ -0,0 +1,45 @@
+---
+name: π Bug Report
+about: Report a reproducible bug
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
+
+## Describe the bug
+
+
+
+## To Reproduce
+
+
+
+```typescript
+// Example code here
+```
+
+## Expected behavior
+
+
+
+## Environment:
+
+* Square Connect Plus Version: x.y.z
+* Node version: x.y.z
+
+## Additional context/Screenshots
+
+
diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.md b/.github/ISSUE_TEMPLATE/2-feature-request.md
new file mode 100644
index 0000000..3532d51
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2-feature-request.md
@@ -0,0 +1,33 @@
+---
+name: β¨ Feature Request
+about: Suggest an idea or feature
+title: 'feat: '
+labels: feature
+assignees: ruscon
+
+---
+
+## Issue Number: [N/A]
+
+## Does this PR introduce a breaking change
+
+ [ ] Yes
+ [ ] No
+
+
+
+## Is your feature request related to a problem? Please describe.
+
+
+
+## Describe the solution you'd like
+
+
+
+## Describe alternatives you've considered
+
+
+
+## Additional context
+
+
diff --git a/.github/ISSUE_TEMPLATE/3-documentation.md b/.github/ISSUE_TEMPLATE/3-documentation.md
new file mode 100644
index 0000000..8f3ddf3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-documentation.md
@@ -0,0 +1,27 @@
+---
+name: π Documentation
+about: Found a typo or something that isn't crystal clear in our docs?
+title: 'docs: '
+labels: documentation
+assignees: ruscon
+
+---
+
+
+
+## Section/Content To Improve
+
+
+
+## Suggested Improvement
+
+
+
+## Relevant File(s):
+
+
diff --git a/.github/ISSUE_TEMPLATE/4-question.md b/.github/ISSUE_TEMPLATE/4-question.md
new file mode 100644
index 0000000..25bcc5d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/4-question.md
@@ -0,0 +1,44 @@
+---
+name: π€ Question
+about: Get help using Square Connect Plus
+title: 'question: '
+labels: question
+assignees: ''
+
+---
+
+
+
+## Environment:
+
+* Square Connect Plus Version: x.y.z
+* Node version: x.y.z
+
+## Describe the issue
+
+
+
+## Example Code
+
+
+
+```typescript
+// Example code here
+```
+
+## Expected behavior, if applicable
+
+
+
+## Additional context/screenshots
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..53b5c75
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+## Summary
+
+
+
+## Context
+
+
diff --git a/.github/config.yml b/.github/config.yml
new file mode 100644
index 0000000..3015da1
--- /dev/null
+++ b/.github/config.yml
@@ -0,0 +1,15 @@
+updateDocsComment: >
+ Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would update some of our documentation based on your changes.
+
+updateDocsWhiteList:
+ - bug
+ - fix
+ - Backport
+ - dev
+ - Update
+ - WIP
+ - chore
+
+updateDocsTargetFiles:
+ - README
+ - docs/
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c391a8a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,63 @@
+name: CI
+
+on: ["push", "pull_request"]
+
+jobs:
+ commitlint:
+ runs-on: ubuntu-latest
+
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Lints Pull Request commits
+ uses: wagoid/commitlint-github-action@v1
+
+ build:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: [10.x, 12.x, 13.x]
+
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - run: node --version
+ - run: npm --version
+
+ - name: Install npm dependencies
+ run: npm i
+
+ - name: Lint code
+ run: npm run lint
+
+ - name: Run tests
+ run: npm run coverage:all
+ env:
+ SQUARE_BASE_URL: ${{ secrets.SQUARE_BASE_URL }}
+ SQUARE_SANDBOX_BASE_URL: ${{ secrets.SQUARE_SANDBOX_BASE_URL }}
+ SQUARE_ACCESS_TOKEN: ${{ secrets.SQUARE_ACCESS_TOKEN }}
+
+ - name: Run Coveralls
+ uses: coverallsapp/github-action@master
+ if: startsWith(matrix.node-version, '12.')
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b070ed4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+# dependencies
+dist/
+node_modules/
+
+# IDE
+/.idea
+/.vscode
+
+# misc
+.DS_Store
+
+# tests
+test.ts
+/test-reports
+/coverage
+/.nyc_output
+/log
+!/bin
+.env
+package-lock.json
diff --git a/.mocharc.json b/.mocharc.json
new file mode 100644
index 0000000..9b65af0
--- /dev/null
+++ b/.mocharc.json
@@ -0,0 +1,15 @@
+{
+ "ui": "bdd",
+ "project": "tsconfig.json",
+ "check-leaks": true,
+ "full-trace": true,
+ "recursive": true,
+ "exit": true,
+ "require": [
+ "ts-node/register/transpile-only",
+ "source-map-support/register",
+ "tsconfig-paths/register",
+ "dotenv-safe/config",
+ "./test/common.opts.ts"
+ ]
+}
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..df3c14a
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+save-exact=false
+scripts-prepend-node-path=true
+package-lock=false
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..dae199a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v12
diff --git a/.nycrc.json b/.nycrc.json
new file mode 100644
index 0000000..7d71873
--- /dev/null
+++ b/.nycrc.json
@@ -0,0 +1,8 @@
+{
+ "extension": [".ts"],
+ "include": ["src/**/*.ts"],
+ "exclude": ["**/*.d.ts", "src/index.ts", "src/interface/index.ts", "src/test.ts"],
+ "reporter": ["lcov", "text-summary", "html"],
+ "cache": false,
+ "all": true
+}
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..736cc1e
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,6 @@
+.nyc_output
+coverage
+dist
+node_modules
+package-lock.json
+src/test.ts
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..b190b4d
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,9 @@
+{
+ "tabWidth": 4,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "arrowParens": "always",
+ "printWidth": 160
+}
diff --git a/.remarkrc b/.remarkrc
new file mode 100644
index 0000000..d1b862a
--- /dev/null
+++ b/.remarkrc
@@ -0,0 +1,13 @@
+{
+ "settings": {
+ "emphasis": "*",
+ "strong": "*",
+ "bullet": "*"
+ },
+ "plugins": [
+ "remark-lint-emphasis-marker",
+ "remark-lint-strong-marker",
+ "remark-github",
+ "remark-frontmatter"
+ ]
+}
diff --git a/.versionrc.json b/.versionrc.json
new file mode 100644
index 0000000..db321f2
--- /dev/null
+++ b/.versionrc.json
@@ -0,0 +1,31 @@
+{
+ "types": [
+ {
+ "type": "feat",
+ "section": "Features"
+ },
+ {
+ "type": "fix",
+ "section": "Bug Fixes"
+ },
+ {
+ "type": "test",
+ "section": "Tests",
+ "hidden": true
+ },
+ {
+ "type": "build",
+ "section": "Build System",
+ "hidden": true
+ },
+ {
+ "type": "chore",
+ "section": "Chore",
+ "hidden": true
+ },
+ {
+ "type": "ci",
+ "hidden": true
+ }
+ ]
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8e489a7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+### [0.0.3](https://github.com/goparrot/square-connect-plus/compare/v0.0.2...v0.0.3) (2020-03-24)
+
+### [0.0.2](https://github.com/goparrot/square-connect-plus/compare/v0.0.1...v0.0.2) (2020-01-31)
+
+### Features
+
+* add SquareException.toObject() method ([d6e194d](https://github.com/goparrot/square-connect-plus/commit/d6e194dc664b6c94b67f0e3c0d40645b71cf1bd4))
+
+### 0.0.1 (2020-01-31)
+
+### Features
+
+* **client:** add client with retry mechanism ([ca771bd](https://github.com/goparrot/square-connect-plus/commit/ca771bd74bf0d39f012566d2c19d9af24d030555))
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..7a099a3
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,27 @@
+## Contributor Code of Conduct
+
+As contributors and maintainers of this project, we pledge to respect all people
+who contribute through reporting issues, posting feature requests, updating
+documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free
+experience for everyone, regardless of level of experience, gender, gender
+identity and expression, sexual orientation, disability, personal appearance,
+body size, race, age, or religion.
+
+Examples of unacceptable behavior by participants include the use of sexual
+language or imagery, derogatory comments or personal attacks, trolling, public
+or private harassment, insults, or other unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct. Project maintainers who do not follow the
+Code of Conduct may be removed from the project team.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by opening an issue or contacting one or more of the project
+maintainers.
+
+This Code of Conduct is adapted from the [Contributor
+Covenant](http:contributor-covenant.org), version 1.0.0, available at
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..27af91f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,32 @@
+# Contributing
+
+First of all, **thank you** for contributing, **you are awesome**!
+
+Here are a few rules to follow in order to ease code reviews, and discussions before
+maintainers accept and merge your work.
+
+You MUST follow the [TypeScript](https://www.typescriptlang.org/docs/home.html) standards. If you don't know about any of them, you
+should really read the recommendations.
+
+Check your node version `node -v`.
+You MUST have `">=10.13"`, because of .
+
+You MUST run the `npm run coverage` command.
+
+You MUST write (or update) unit tests.
+
+You MUST run the `npm run commit` hook to add a new commit.
+
+You SHOULD write documentation.
+
+Please, write [commit messages that make
+sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
+and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
+before submitting your Pull Request.
+
+One may ask you to [squash your
+commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
+too. This is used to "clean" your Pull Request before merging it (we don't want
+commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
+
+Thank you!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..be7581f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2017 Kamil MyΕliwiec
+
+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:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+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.
diff --git a/README.md b/README.md
index 04332de..05c4d1e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,116 @@
-# square-connect-plus
-Typescript library which extends the official Square Connect APIs library with additional functionality
+[](https://github.com/goparrot/square-connect-plus/actions?query=branch%3Amaster+event%3Apush+workflow%3ACI)
+[](https://coveralls.io/github/goparrot/square-connect-plus?branch=master)
+[](https://www.npmjs.com/package/@goparrot/square-connect-plus)
+[](https://greenkeeper.io/)
+[](http://commitizen.github.io/cz-cli/)
+[](https://conventionalcommits.org)
+
+# Square Connect Plus
+
+**Square Connect Plus** is a Typescript library which extends the official Square Connect APIs library with additional functionality.
+The library does not modify request and response payload.
+
+* [Installation](#installation)
+* [Usage](#usage)
+* [Versioning](#versioning)
+* [Contributing](#contributing)
+* [Unit Tests](#unit-tests)
+* [Background](#background)
+* [License](#license)
+
+## Installation
+
+ $ npm i @goparrot/square-connect-plus square-connect
+
+## Usage
+
+### Simple example
+
+```typescript
+import { SquareClient } from '@goparrot/square-connect-plus';
+import { ListLocationsResponse } from 'square-connect';
+
+const accessToken: string = `${process.env.SQUARE_ACCESS_TOKEN}`;
+const squareClient: SquareClient = new SquareClient(accessToken);
+
+(async () => {
+ try {
+ const listLocationsResponse: ListLocationsResponse = await squareClient.getLocationsApi().listLocations();
+ if (listLocationsResponse.errors) {
+ throw new Error(`cant fetch locations`);
+ }
+
+ console.info('locations', listLocationsResponse.locations);
+ } catch (error) {
+ console.error(error);
+ // or error as string with stack + request and response payload
+ // console.error(`${error.stack}\npayload: ${error.toString()}`);
+ }
+})();
+```
+
+### Advanced example
+
+```typescript
+import { SquareClient, exponentialDelay, retryCondition } from '@goparrot/square-connect-plus';
+
+const accessToken: string = `${process.env.SQUARE_ACCESS_TOKEN}`;
+const squareClient: SquareClient = new SquareClient(accessToken, {
+ retry: {
+ maxRetries: 10,
+ },
+ originClient: {
+ timeout: 10000,
+ },
+ logger: console,
+});
+```
+
+## Available Options
+
+### `retry` Options
+
+| Name | Type | Default | Description |
+| -------------- | ---------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| maxRetries | `Number` | `6` | The number of times to retry before failing. |
+| retryCondition | `Function` | `retryCondition` | A callback to further control if a request should be retried. By default, the built-in `retryCondition` function is used. |
+| retryDelay | `Function` | `exponentialDelay` | A callback to further control the delay between retried requests. By default, the built-in `exponentialDelay` function is used ([Exponential Backoff](https://developers.google.com/analytics/devguides/reporting/core/v3/errors#backoff)). |
+
+### `originClient` Options
+
+A set of possible settings for the original library.
+
+| Name | Type | Default | Description |
+| -------------- | --------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
+| basePath | `String` | `https://connect.squareup.com` | The base URL against which to resolve every API call's (relative) path. |
+| defaultHeaders | `Array` | `{ 'User-Agent': 'Square-Connect-Javascript/3.20200226.0' }` | The default HTTP headers to be included for all API calls. |
+| timeout | `Number` | `15000` | The default HTTP timeout for all API calls. |
+| cache | `Boolean` | `true` | If set to false an additional timestamp parameter is added to all API GET calls to prevent browser caching. |
+| enableCookies | `Boolean` | `false` | If set to true, the client will save the cookies from each server response, and return them in the next request. |
+
+### `logger` Option
+
+By default, the built-in `NullLogger` class is used.
+You can use any logger that fits the built-in `ILogger` interface
+
+## Versioning
+
+Square Connect Plus follows [Semantic Versioning](http://semver.org/).
+
+## Contributing
+
+See [`CONTRIBUTING`](https://github.com/goparrot/square-connect-plus/blob/master/CONTRIBUTING.md#contributing) file.
+
+## Unit Tests
+
+In order to run the test suite, install the development dependencies:
+
+ $ npm i
+
+Then, run the following command:
+
+ $ npm run coverage
+
+## License
+
+Square Connect Plus is [MIT licensed](LICENSE).
diff --git a/bin/prepublish.ts b/bin/prepublish.ts
new file mode 100644
index 0000000..b22a384
--- /dev/null
+++ b/bin/prepublish.ts
@@ -0,0 +1,21 @@
+import { writeFileSync, copyFileSync } from 'fs';
+import originPackage from '../package.json';
+
+const distPackage: Record = originPackage;
+distPackage.module = './index.js';
+distPackage.main = './index.js';
+distPackage.types = './index.d.ts';
+delete distPackage.scripts;
+delete distPackage.devDependencies;
+delete distPackage.config;
+delete distPackage.husky;
+delete distPackage.files;
+delete distPackage.directories;
+delete distPackage['lint-staged'];
+
+writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, ' '));
+
+const copyFiles = ['README.md'];
+for (const file of copyFiles) {
+ copyFileSync(`./${file}`, `./dist/${file}`);
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1ef6693
--- /dev/null
+++ b/package.json
@@ -0,0 +1,121 @@
+{
+ "name": "@goparrot/square-connect-plus",
+ "description": "Extends the official Square Connect APIs Javascript library with additional functionality",
+ "version": "0.0.3",
+ "author": "Coroliov Oleg",
+ "license": "MIT",
+ "private": false,
+ "bugs": {
+ "url": "https://github.com/goparrot/square-connect-plus/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/goparrot/square-connect-plus.git"
+ },
+ "keywords": [
+ "node",
+ "typescript",
+ "square-connect",
+ "retry"
+ ],
+ "engines": {
+ "node": ">=10"
+ },
+ "main": "src/index.ts",
+ "husky": {
+ "hooks": {
+ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS && npm run pre-commit",
+ "post-commit": "git update-index --again"
+ }
+ },
+ "lint-staged": {
+ "*.{ts,tsx,json,js,jsx}": [
+ "npm run format:base"
+ ]
+ },
+ "config": {
+ "commitizen": {
+ "path": "cz-conventional-changelog"
+ }
+ },
+ "scripts": {
+ "commit": "git-cz",
+ "test": "mocha 'test/unit/**/*.spec.ts' 'test/e2e/**/*.spec.ts'",
+ "test:all": "mocha --timeout 15000 'test/**/*.spec.ts'",
+ "test:unit": "mocha 'test/unit/**/*.spec.ts'",
+ "test:e2e": "mocha 'test/e2e/**/*.spec.ts'",
+ "test:integration": "mocha --timeout 15000 'test/integration/**/*.spec.ts'",
+ "coverage": "nyc npm test",
+ "coverage:all": "nyc npm run test:all",
+ "lint": "npm run lint:base -- '.'",
+ "lint:base": "npm run lint:config:check && eslint --ignore-path .gitignore --ext .ts,.tsx,.json,.js,.jsx",
+ "lint:config:check": "eslint --print-config src/index.ts | eslint-config-prettier-check",
+ "format": "npm run format:base -- '.'",
+ "format:base": "npm run lint:base -- --fix",
+ "format:staged": "git add . && lint-staged",
+ "build": "rimraf dist && tsc -p tsconfig.build.json",
+ "remark": "remark README.md CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md .github/ -o -f -q && git add .",
+ "pre-commit": "git add . && npm run format:staged && npm run remark && npm run lint && npm run coverage:all && npm run build",
+ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && remark CHANGELOG.md -o -f -q && git add CHANGELOG.md",
+ "prepublishOnly": "echo \"use 'npm run publish'\" && exit 1",
+ "publish": "npm run build && ts-node -T bin/prepublish.ts && npm publish dist",
+ "publish:dev": "npm run publish --tag dev",
+ "publish:dev:dry": "npm run publish:dev --dry-run",
+ "version": "echo \"use 'npm run release'\" && exit 1",
+ "release": "standard-version && git push && git push --tags && npm run publish && npm run github-release",
+ "release:dry": "npm run publish:dev:dry && standard-version --dry-run",
+ "github-release": "env-cmd conventional-github-releaser -p angular"
+ },
+ "peerDependencies": {
+ "square-connect": "^3.20200226.0"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "@commitlint/cli": "^8.3.5",
+ "@commitlint/config-conventional": "^8.3.4",
+ "@commitlint/travis-cli": "^8.2.0",
+ "@types/chai": "^4.2.11",
+ "@types/chai-as-promised": "^7.1.2",
+ "@types/mocha": "^7.0.2",
+ "@types/node": "^13.9.3",
+ "@types/sinon": "^7.5.2",
+ "@types/square-connect": "^2.20190814.3",
+ "@types/superagent": "^4.1.7",
+ "@typescript-eslint/eslint-plugin": "^2.25.0",
+ "@typescript-eslint/parser": "^3.0.0",
+ "chai": "^4.2.0",
+ "chai-as-promised": "^7.1.1",
+ "commitizen": "^4.0.0",
+ "conventional-changelog-cli": "^2.0.27",
+ "conventional-github-releaser": "^3.1.3",
+ "cz-conventional-changelog": "^3.1.0",
+ "dotenv-safe": "^8.2.0",
+ "env-cmd": "^10.1.0",
+ "eslint": "^6.8.0",
+ "eslint-config-prettier": "^6.10.1",
+ "eslint-import-resolver-typescript": "^2.0.0",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^4.2.3",
+ "lint-staged": "^10.0.9",
+ "mocha": "^7.1.1",
+ "mocha-junit-reporter": "^1.23.0",
+ "nock": "^12.0.3",
+ "nyc": "^15.0.0",
+ "prettier": "^2.0.2",
+ "remark-cli": "^7.0.0",
+ "remark-frontmatter": "^1.3.3",
+ "remark-github": "^9.0.0",
+ "remark-lint-emphasis-marker": "^1.0.3",
+ "remark-lint-strong-marker": "^1.0.3",
+ "rimraf": "^3.0.2",
+ "sinon": "^9.0.1",
+ "source-map-support": "^0.5.16",
+ "square-connect": "^3.20200226.0",
+ "standard-version": "^7.0.0",
+ "superagent": "^5.2.2",
+ "ts-node": "^8.8.1",
+ "tsconfig-paths": "^3.9.0",
+ "typescript": "^3.8.3"
+ }
+}
diff --git a/src/client/index.ts b/src/client/index.ts
new file mode 100644
index 0000000..5adabda
--- /dev/null
+++ b/src/client/index.ts
@@ -0,0 +1,2 @@
+export * from './square-client';
+export * from './square-client-factory';
diff --git a/src/client/square-client-factory.ts b/src/client/square-client-factory.ts
new file mode 100644
index 0000000..d6972e3
--- /dev/null
+++ b/src/client/square-client-factory.ts
@@ -0,0 +1,12 @@
+import { ISquareClientConfig } from '../interface';
+import { SquareClient } from './square-client';
+
+export class SquareClientFactory {
+ static create(accessToken: string, config: ISquareClientConfig = {}): SquareClient {
+ return new SquareClient(accessToken, config);
+ }
+
+ create(accessToken: string, config: ISquareClientConfig = {}): SquareClient {
+ return SquareClientFactory.create(accessToken, config);
+ }
+}
diff --git a/src/client/square-client.ts b/src/client/square-client.ts
new file mode 100644
index 0000000..c9b1142
--- /dev/null
+++ b/src/client/square-client.ts
@@ -0,0 +1,201 @@
+import {
+ ApiClient,
+ ApplePayApi,
+ CatalogApi,
+ CheckoutApi,
+ CustomersApi,
+ EmployeesApi,
+ InventoryApi,
+ LaborApi,
+ LocationsApi,
+ MobileAuthorizationApi,
+ OAuthApi,
+ OrdersApi,
+ PaymentsApi,
+ RefundsApi,
+ TransactionsApi,
+} from 'square-connect';
+import { ISquareClientConfig, ISquareClientDefaultConfig, ISquareClientMergedConfig } from '../interface';
+import { ILogger, NullLogger } from '../logger';
+import { exponentialDelay, makeRetryable, mergeDeepProps, retryCondition } from '../utils';
+
+export class SquareClient {
+ private originApiClient: ApiClient;
+ private readonly config: ISquareClientMergedConfig;
+ private readonly defaultConfig: ISquareClientDefaultConfig = {
+ retry: {
+ maxRetries: 6,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 15000,
+ },
+ };
+
+ constructor(private readonly accessToken: string, config: ISquareClientConfig = {}) {
+ this.config = mergeDeepProps(this.defaultConfig, config);
+ this.config.logger = config.logger;
+ }
+
+ /**
+ * Generate unique idempotency key (format: reference-timestampWithMilliseconds)
+ */
+ static generateIdempotencyKey(reference: string): string {
+ return [reference, Date.now().toString()].join('-');
+ }
+
+ getConfig(): ISquareClientMergedConfig {
+ return this.config;
+ }
+
+ getOriginApiClient(): ApiClient {
+ return this.originApiClient ?? (this.originApiClient = this.createOriginApiClient(this.accessToken, this.config));
+ }
+
+ getApplePayApi(): ApplePayApi {
+ const retryableMethods: string[] = [];
+
+ return this.proxify(new ApplePayApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getCatalogApi(): CatalogApi {
+ const retryableMethods: string[] = ['batchRetrieveCatalogObjects', 'catalogInfo', 'listCatalog', 'retrieveCatalogObject', 'searchCatalogObjects'];
+
+ return this.proxify(new CatalogApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getCheckoutApi(): CheckoutApi {
+ const retryableMethods: string[] = [];
+
+ return this.proxify(new CheckoutApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getCustomersApi(): CustomersApi {
+ // createCustomerCard should not be retryable (#GP-2400)
+ const retryableMethods: string[] = ['listCustomers', 'retrieveCustomer', 'searchCustomers', 'deleteCustomerCard'];
+
+ return this.proxify(new CustomersApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getEmployeesApi(): EmployeesApi {
+ const retryableMethods: string[] = ['listEmployees', 'retrieveEmployee'];
+
+ return this.proxify(new EmployeesApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getInventoryApi(): InventoryApi {
+ const retryableMethods: string[] = [
+ 'batchRetrieveInventoryChanges',
+ 'batchRetrieveInventoryCounts',
+ 'retrieveInventoryAdjustment',
+ 'retrieveInventoryChanges',
+ 'retrieveInventoryCount',
+ 'retrieveInventoryPhysicalCount',
+ ];
+
+ return this.proxify(new InventoryApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getLaborApi(): LaborApi {
+ const retryableMethods: string[] = [
+ 'getBreakType',
+ 'getEmployeeWage',
+ 'getShift',
+ 'listBreakTypes',
+ 'listEmployeeWages',
+ 'listWorkweekConfigs',
+ 'searchShifts',
+ ];
+
+ return this.proxify(new LaborApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getLocationsApi(): LocationsApi {
+ const retryableMethods: string[] = ['listLocations'];
+
+ return this.proxify(new LocationsApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getMobileAuthorizationApi(): MobileAuthorizationApi {
+ const retryableMethods: string[] = [];
+
+ return this.proxify(new MobileAuthorizationApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getOAuthApi(): OAuthApi {
+ const retryableMethods: string[] = [];
+
+ return this.proxify(new OAuthApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getOrdersApi(): OrdersApi {
+ const retryableMethods: string[] = ['batchRetrieveOrders', 'searchOrders', 'createOrder', 'payOrder'];
+
+ return this.proxify(new OrdersApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getPaymentsApi(): PaymentsApi {
+ const retryableMethods: string[] = ['getPayment', 'listPayments', 'createPayment', 'cancelPayment'];
+
+ return this.proxify(new PaymentsApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getRefundsApi(): RefundsApi {
+ const retryableMethods: string[] = ['getPaymentRefund', 'listPaymentRefunds', 'refundPayment'];
+
+ return this.proxify(new RefundsApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ getTransactionsApi(): TransactionsApi {
+ const retryableMethods: string[] = ['listRefunds', 'listTransactions', 'retrieveTransaction'];
+
+ return this.proxify(new TransactionsApi(this.getOriginApiClient()), retryableMethods);
+ }
+
+ private createOriginApiClient(accessToken: string, config: ISquareClientConfig): ApiClient {
+ const apiClient: ApiClient = new ApiClient();
+ apiClient.authentications.oauth2.accessToken = accessToken;
+
+ return mergeDeepProps(apiClient, config.originClient ?? {});
+ }
+
+ private getLogger(): ILogger {
+ return this.config.logger ?? (this.config.logger = new NullLogger());
+ }
+
+ private proxify(api: T, retryableMethods: string[]): T {
+ let globalStack: string = '';
+ try {
+ throw new Error();
+ } catch (err) {
+ globalStack += err.stack.slice(6); // remove "Error:"
+ }
+
+ const handler: ProxyHandler = {
+ get: (target: T, apiMethodName: string): any => {
+ if (!retryableMethods.includes(apiMethodName)) {
+ return target[apiMethodName];
+ }
+
+ return async (...args: any[]): Promise => {
+ const requestFn: () => any = target[apiMethodName].bind(target, ...args);
+
+ this.getLogger().debug(`Square api request: ${JSON.stringify({ apiMethodName, args })}`);
+
+ try {
+ return await makeRetryable(requestFn, {
+ maxRetries: this.config.retry.maxRetries,
+ retryDelay: this.config.retry.retryDelay,
+ retryCondition: this.config.retry.retryCondition ?? retryCondition,
+ });
+ } catch (err) {
+ err.stack += globalStack;
+ throw err;
+ }
+ };
+ },
+ };
+
+ return new Proxy(api, handler);
+ }
+}
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..4234712
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,9 @@
+import { ErrorCodeType } from 'square-connect';
+
+export const retryableErrorCodes: ReadonlyArray = Object.freeze([
+ 'RATE_LIMITED',
+ 'REQUEST_TIMEOUT',
+ 'GATEWAY_TIMEOUT',
+ 'SERVICE_UNAVAILABLE',
+ 'INTERNAL_SERVER_ERROR',
+]);
diff --git a/src/exception/i-square-exception.ts b/src/exception/i-square-exception.ts
new file mode 100644
index 0000000..4391a9e
--- /dev/null
+++ b/src/exception/i-square-exception.ts
@@ -0,0 +1,12 @@
+import { ModelError } from 'square-connect';
+
+export interface ISquareException {
+ name: string;
+ statusCode: number;
+ message: string;
+ apiError: ModelError;
+ retries?: number;
+ url?: string;
+ method?: string;
+ requestArgs?: any;
+}
diff --git a/src/exception/index.ts b/src/exception/index.ts
new file mode 100644
index 0000000..b2883bc
--- /dev/null
+++ b/src/exception/index.ts
@@ -0,0 +1,2 @@
+export * from './i-square-exception';
+export * from './square-exception';
diff --git a/src/exception/square-exception.ts b/src/exception/square-exception.ts
new file mode 100644
index 0000000..a4b6ba2
--- /dev/null
+++ b/src/exception/square-exception.ts
@@ -0,0 +1,88 @@
+import { ModelError } from 'square-connect';
+import { Response, SuperAgentRequest } from 'superagent';
+import { ISquareException } from './i-square-exception';
+
+export class SquareException extends Error implements ISquareException {
+ statusCode: number;
+ apiError: ModelError;
+ retries?: number;
+ url?: string;
+ method?: string;
+ requestArgs?: any;
+
+ constructor(
+ data?: { retries?: number; url?: string; method?: string; statusCode?: number; apiError?: ModelError; requestArgs?: any },
+ originError?: Error,
+ ) {
+ super();
+
+ this.name = this.constructor.name;
+
+ this.retries = data?.retries || 0;
+ this.url = data?.url;
+ this.method = data?.method?.toUpperCase();
+ this.statusCode = data?.statusCode || 500;
+ this.apiError = data?.apiError || { category: 'API_ERROR', code: 'SERVICE_UNAVAILABLE', detail: 'Square API error' };
+ this.message = this.apiError.detail ?? this.apiError.code ?? originError?.message ?? 'Square API error';
+ this.requestArgs = data?.requestArgs;
+
+ // Error.captureStackTrace(this);
+ }
+
+ static createFromSuperAgentError(
+ error: Error & { response?: Response & { request?: SuperAgentRequest }; code?: string },
+ retries: number,
+ ): SquareException {
+ const response: (Response & { request?: SuperAgentRequest }) | undefined = error?.response;
+ const request: SuperAgentRequest | undefined = response?.request;
+
+ if (response) {
+ // console.dir(request, {depth: undefined});
+ // console.log(JSON.parse(JSON.stringify(response)));
+ return new SquareException(
+ {
+ retries,
+ url: request?.url,
+ statusCode: response?.status,
+ method: request?.method,
+ apiError: response?.body?.errors?.[0],
+ // @ts-ignore
+ requestArgs: request?._data,
+ },
+ error,
+ );
+ } else if ('ECONNABORTED' === error.code) {
+ return new SquareException({
+ retries,
+ statusCode: 500,
+ apiError: { category: 'API_ERROR', code: 'GATEWAY_TIMEOUT', detail: 'Square API timeout' },
+ });
+ }
+
+ return new SquareException(
+ {
+ retries,
+ statusCode: 500,
+ apiError: { category: 'API_ERROR', code: 'SERVICE_UNAVAILABLE', detail: 'Square API error' },
+ },
+ error,
+ );
+ }
+
+ toObject(): ISquareException {
+ return {
+ name: this.name,
+ message: this.message,
+ retries: this.retries,
+ url: this.url,
+ method: this.method,
+ statusCode: this.statusCode,
+ requestArgs: this.requestArgs,
+ apiError: this.apiError,
+ };
+ }
+
+ toString(): string {
+ return JSON.stringify(this.toObject());
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..650c8fc
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,6 @@
+export * from './exception';
+export * from './interface';
+export * from './logger';
+export * from './utils';
+export * from './client';
+export * from './constants';
diff --git a/src/interface/i-square-api-config.ts b/src/interface/i-square-api-config.ts
new file mode 100644
index 0000000..27d5afa
--- /dev/null
+++ b/src/interface/i-square-api-config.ts
@@ -0,0 +1,33 @@
+import { ILogger } from '../logger';
+import { IRetriesOptions } from '../utils';
+
+export interface ISquareClientConfig {
+ retry?: Partial;
+ originClient?: {
+ /**
+ * The base URL against which to resolve every API call's (relative) path.
+ */
+ basePath?: string;
+
+ /**
+ * The default HTTP headers to be included for all API calls.
+ */
+ defaultHeaders?: { [key: string]: string };
+
+ /**
+ * The default HTTP timeout for all API calls.
+ */
+ timeout?: number;
+
+ /**
+ * If set to false an additional timestamp parameter is added to all API GET calls to prevent browser caching.
+ */
+ cache?: boolean;
+
+ /**
+ * If set to true, the client will save the cookies from each server response, and return them in the next request.
+ */
+ enableCookies?: boolean;
+ };
+ logger?: ILogger;
+}
diff --git a/src/interface/i-square-api-default-config.ts b/src/interface/i-square-api-default-config.ts
new file mode 100644
index 0000000..43e8b91
--- /dev/null
+++ b/src/interface/i-square-api-default-config.ts
@@ -0,0 +1,9 @@
+import { ISquareClientConfig } from './i-square-api-config';
+import { ISquareClientDefaultRetryConfig } from './i-square-api-default-retry-config';
+
+export interface ISquareClientDefaultConfig extends ISquareClientConfig {
+ retry: ISquareClientDefaultRetryConfig;
+ originClient: {
+ timeout: number;
+ };
+}
diff --git a/src/interface/i-square-api-default-retry-config.ts b/src/interface/i-square-api-default-retry-config.ts
new file mode 100644
index 0000000..8aee8fc
--- /dev/null
+++ b/src/interface/i-square-api-default-retry-config.ts
@@ -0,0 +1,6 @@
+import { IRetriesOptions } from '../utils';
+
+export interface ISquareClientDefaultRetryConfig extends Partial {
+ maxRetries: number;
+ retryDelay: (retryCount: number) => number;
+}
diff --git a/src/interface/i-square-api-merged-config.ts b/src/interface/i-square-api-merged-config.ts
new file mode 100644
index 0000000..b0c5405
--- /dev/null
+++ b/src/interface/i-square-api-merged-config.ts
@@ -0,0 +1,3 @@
+import { ISquareClientDefaultConfig } from './i-square-api-default-config';
+
+export interface ISquareClientMergedConfig extends ISquareClientDefaultConfig {}
diff --git a/src/interface/index.ts b/src/interface/index.ts
new file mode 100644
index 0000000..6d69679
--- /dev/null
+++ b/src/interface/index.ts
@@ -0,0 +1,4 @@
+export * from './i-square-api-config';
+export * from './i-square-api-default-retry-config';
+export * from './i-square-api-default-config';
+export * from './i-square-api-merged-config';
diff --git a/src/logger/i-logger.ts b/src/logger/i-logger.ts
new file mode 100644
index 0000000..52b7c47
--- /dev/null
+++ b/src/logger/i-logger.ts
@@ -0,0 +1,6 @@
+export interface ILogger {
+ debug(message: string, meta?: any): any;
+ info(message: string, meta?: any): any;
+ warn(message: string, meta?: any): any;
+ error(message: string, meta?: any): any;
+}
diff --git a/src/logger/index.ts b/src/logger/index.ts
new file mode 100644
index 0000000..6d95c9c
--- /dev/null
+++ b/src/logger/index.ts
@@ -0,0 +1,2 @@
+export * from './null-logger';
+export * from './i-logger';
diff --git a/src/logger/null-logger.ts b/src/logger/null-logger.ts
new file mode 100644
index 0000000..06bf610
--- /dev/null
+++ b/src/logger/null-logger.ts
@@ -0,0 +1,19 @@
+import { ILogger } from './i-logger';
+
+export class NullLogger implements ILogger {
+ debug(): any {
+ //
+ }
+
+ info(): any {
+ //
+ }
+
+ warn(): any {
+ //
+ }
+
+ error(): any {
+ //
+ }
+}
diff --git a/src/utils/common.utils.ts b/src/utils/common.utils.ts
new file mode 100644
index 0000000..e1ed6d7
--- /dev/null
+++ b/src/utils/common.utils.ts
@@ -0,0 +1,42 @@
+export async function sleep(timeout: number): Promise {
+ return new Promise((resolve: any): any => setTimeout(resolve, timeout));
+}
+
+/**
+ * Simple object check.
+ * @returns {boolean}
+ */
+export function isObject(item?: T): item is T {
+ return !!item && 'object' === typeof item && !Array.isArray(item);
+}
+
+/**
+ * @link {https://stackoverflow.com/a/34749873/3408246}
+ * Deep merge props of two objects
+ */
+export function mergeDeepProps(target: T, ...sources: S[]): T {
+ if (!sources.length) return target;
+ const source: S | undefined = sources.shift();
+
+ if (isObject(target) && isObject(source)) {
+ for (const key in source) {
+ if (isObject(source[key])) {
+ // @ts-ignore
+ if (!target[key]) {
+ Object.assign(target, { [key]: {} });
+ }
+
+ mergeDeepProps(
+ // @ts-ignore
+ target[key],
+ // @ts-ignore
+ source[key],
+ );
+ } else {
+ Object.assign(target, { [key]: source[key] });
+ }
+ }
+ }
+
+ return mergeDeepProps(target, ...sources);
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..d5f66ed
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './common.utils';
+export * from './retry.utils';
diff --git a/src/utils/retry.utils.ts b/src/utils/retry.utils.ts
new file mode 100644
index 0000000..35b1c7f
--- /dev/null
+++ b/src/utils/retry.utils.ts
@@ -0,0 +1,67 @@
+import { retryableErrorCodes } from '../constants';
+import { SquareException } from '../exception';
+import { sleep } from './common.utils';
+
+export interface IRetriesOptions {
+ /** @default 3 */
+ maxRetries: number;
+ /** @default exponentialDelay */
+ retryDelay: (retryCount: number) => number;
+ retryCondition: (error: SquareException, maxRetries: number, retries: number) => Promise;
+}
+
+/**
+ * @return {number} - delay in milliseconds
+ */
+export function exponentialDelay(retryNumber: number): number {
+ const delay: number = Math.pow(2, retryNumber) * 100;
+ const randomSum: number = delay * 0.2 * Math.random(); // 0-20% of the delay
+
+ return delay + randomSum;
+}
+
+/**
+ * add the ability to retry the request
+ */
+export async function makeRetryable(promiseFn: (...arg: any[]) => Promise, params: IRetriesOptions): Promise {
+ let retries: number = 0;
+
+ async function retry(): Promise {
+ try {
+ return await promiseFn();
+ } catch (error) {
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, retries);
+
+ if (await params.retryCondition(squareException, params.maxRetries, retries)) {
+ retries++;
+ const delay: number = exponentialDelay(retries);
+ await sleep(delay);
+
+ return retry();
+ }
+
+ throw squareException;
+ }
+ }
+
+ return retry();
+}
+
+export function isRetryableException(error: Error): boolean {
+ if (!(error instanceof SquareException)) {
+ return false;
+ }
+
+ const isRetryableResponseStatusCode: boolean = 429 === error.statusCode || (error.statusCode >= 500 && 501 !== error.statusCode);
+ const isIdempotentRequestMethod: boolean = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'].includes(error.method?.toUpperCase() || '');
+
+ return isRetryableResponseStatusCode && (isIdempotentRequestMethod || retryableErrorCodes.includes(error.apiError.code));
+}
+
+export async function retryCondition(error: SquareException, maxRetries: number, retries: number): Promise {
+ if (isRetryableException(error) && maxRetries > retries) {
+ return true;
+ }
+
+ throw error;
+}
diff --git a/test/common.opts.ts b/test/common.opts.ts
new file mode 100644
index 0000000..59bfe15
--- /dev/null
+++ b/test/common.opts.ts
@@ -0,0 +1,7 @@
+import * as chai from 'chai';
+// @ts-ignore
+import chaiAsPromised from 'chai-as-promised';
+
+chai.use(chaiAsPromised);
+// @ts-ignore
+global.should = chai.should();
diff --git a/test/e2e/client/square-client.spec.ts b/test/e2e/client/square-client.spec.ts
new file mode 100644
index 0000000..967e9af
--- /dev/null
+++ b/test/e2e/client/square-client.spec.ts
@@ -0,0 +1,104 @@
+import nock from 'nock';
+import { SquareClient } from '../../../src/client';
+import { SquareException } from '../../../src/exception';
+import { ISquareClientConfig } from '../../../src/interface';
+import { exponentialDelay } from '../../../src/utils';
+
+describe('SquareClient (e2e)', (): void => {
+ const accessToken: string = 'test';
+ const basePath: string = `${process.env.SQUARE_SANDBOX_BASE_URL}`;
+
+ const config: ISquareClientConfig = {
+ retry: {
+ maxRetries: 2,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 100,
+ cache: false,
+ enableCookies: false,
+ basePath,
+ },
+ };
+
+ afterEach((): void => {
+ nock.cleanAll();
+ });
+
+ it('should NOT retry 501 http status', async (): Promise => {
+ nock(basePath).get(/.*/).times(1000).reply(501);
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException)
+ .and.have.property('retries', 0);
+ });
+
+ it('should NOT retry 400 http status', async (): Promise => {
+ nock(basePath).get(/.*/).times(1000).reply(400);
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException)
+ .and.have.property('retries', 0);
+ });
+
+ it('should retry 429 http status', async (): Promise => {
+ nock(basePath)
+ .get(/.*/)
+ .times(1000)
+ .reply(429, {
+ errors: [
+ {
+ category: 'RATE_LIMIT_ERROR',
+ code: 'RATE_LIMITED',
+ detail: 'fake 429 error',
+ },
+ ],
+ });
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException, 'fake 429 error')
+ .and.have.property('retries', config.retry?.maxRetries);
+ });
+
+ it('should retry 500 http status', async (): Promise => {
+ nock(basePath).get(/.*/).times(1000).reply(500);
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException)
+ .and.have.property('retries', config.retry?.maxRetries);
+ });
+
+ it('should retry 503 http status', async (): Promise => {
+ nock(basePath).get(/.*/).times(1000).reply(503);
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException)
+ .and.have.property('retries', config.retry?.maxRetries);
+ });
+
+ it('should retry if request timeout', async (): Promise => {
+ nock(basePath)
+ .get(/.*/)
+ .times(1000)
+ .delay({
+ head: 3000,
+ })
+ .reply(200, 'OK');
+
+ return new SquareClient(accessToken, config)
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException, 'Square API timeout')
+ .and.have.property('retries', config.retry?.maxRetries);
+ });
+});
diff --git a/test/integration/client/square-client.spec.ts b/test/integration/client/square-client.spec.ts
new file mode 100644
index 0000000..4d48c8c
--- /dev/null
+++ b/test/integration/client/square-client.spec.ts
@@ -0,0 +1,57 @@
+import { SearchCustomersRequest } from 'square-connect';
+import { SquareClient } from '../../../src/client';
+import { SquareException } from '../../../src/exception';
+import { ISquareClientConfig } from '../../../src/interface';
+
+describe('SquareClient (integration)', (): void => {
+ const accessToken: string = `${process.env.SQUARE_ACCESS_TOKEN}`;
+ const basePath: string = `${process.env.SQUARE_BASE_URL}`;
+
+ const config: ISquareClientConfig = {
+ retry: {
+ maxRetries: 1,
+ },
+ originClient: {
+ timeout: 10000,
+ basePath,
+ },
+ };
+
+ describe('#getLocationsApi', (): void => {
+ it('should retry by timeout', async (): Promise => {
+ return new SquareClient(accessToken, {
+ ...config,
+ ...{
+ originClient: {
+ timeout: 1,
+ },
+ },
+ })
+ .getLocationsApi()
+ .listLocations()
+ .should.eventually.be.rejectedWith(SquareException, 'Square API timeout');
+ });
+
+ it('should retrieve data', async (): Promise => {
+ return new SquareClient(accessToken, config).getLocationsApi().listLocations().should.eventually.be.fulfilled.and.have.property('locations');
+ });
+ });
+
+ describe('#getCustomersApi', (): void => {
+ it('should be rejected with ModelError', async (): Promise => {
+ const query: SearchCustomersRequest = {
+ limit: 1,
+ query: {
+ sort: {
+ order: 'WRONG_VALUE' as any,
+ },
+ },
+ };
+
+ return new SquareClient(accessToken, config)
+ .getCustomersApi()
+ .searchCustomers(query)
+ .should.eventually.be.rejectedWith(Error, '`WRONG_VALUE` is not a valid enum value for `query.sort.order`.');
+ });
+ });
+});
diff --git a/test/unit/client/square-client-factory.spec.ts b/test/unit/client/square-client-factory.spec.ts
new file mode 100644
index 0000000..7e586a4
--- /dev/null
+++ b/test/unit/client/square-client-factory.spec.ts
@@ -0,0 +1,127 @@
+import { ApiClient, CustomersApi, LocationsApi, OrdersApi, PaymentsApi, RefundsApi } from 'square-connect';
+import { SquareClient, SquareClientFactory } from '../../../src/client';
+import { ISquareClientConfig } from '../../../src/interface';
+import { exponentialDelay } from '../../../src/utils';
+
+describe('SquareClientFactory (unit)', (): void => {
+ const accessToken: string = 'test';
+
+ const config: ISquareClientConfig = {
+ retry: {
+ maxRetries: 1,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 1000,
+ cache: false,
+ enableCookies: true,
+ basePath: 'https://connect.squareupsandbox.com',
+ },
+ logger: console,
+ };
+
+ describe('#create', (): void => {
+ it('should be statically init with accessToken only (default config)', async (): Promise => {
+ return SquareClientFactory.create(accessToken).should.be.instanceOf(SquareClient);
+ });
+
+ it('should be init with accessToken only (default config)', async (): Promise => {
+ return new SquareClientFactory().create(accessToken).should.be.instanceOf(SquareClient);
+ });
+
+ it('should be statically init with accessToken and config', async (): Promise => {
+ return SquareClientFactory.create(accessToken, config).should.be.instanceOf(SquareClient);
+ });
+
+ it('should be init with accessToken and config', async (): Promise => {
+ return new SquareClientFactory().create(accessToken, config).should.be.instanceOf(SquareClient);
+ });
+ });
+
+ describe('#generateIdempotencyKey', (): void => {
+ it('should return string', async (): Promise => {
+ const reference: string = 'test';
+ return SquareClient.generateIdempotencyKey(reference)
+ .should.be.match(new RegExp(`^${reference}-.*`))
+ .and.lengthOf(18);
+ });
+ });
+
+ describe('#getConfig', (): void => {
+ it('should return default configuration', async (): Promise => {
+ return new SquareClient(accessToken).getConfig().should.be.deep.eq({
+ retry: {
+ maxRetries: 6,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 15000,
+ },
+ logger: undefined,
+ });
+ });
+
+ it('should return custom configuration', async (): Promise => {
+ return new SquareClient(accessToken, config).getConfig().should.be.deep.eq(config);
+ });
+ });
+
+ describe('#getOriginApiClient', (): void => {
+ it('should return ApiClient with default configuration', async (): Promise => {
+ const apiClient: ApiClient = new SquareClient(accessToken).getOriginApiClient();
+
+ apiClient.should.be.instanceOf(ApiClient);
+ apiClient.should.have.property('timeout', 15000);
+ apiClient.should.have.property('cache', true);
+ apiClient.should.have.property('enableCookies', false);
+ apiClient.should.have.property('defaultHeaders').and.should.be.a('object');
+ return apiClient.should.and.have.property('basePath', 'https://connect.squareup.com');
+ });
+
+ it('should return ApiClient with custom configuration', async (): Promise => {
+ const apiClient: ApiClient = new SquareClient(accessToken, config).getOriginApiClient();
+
+ apiClient.should.be.instanceOf(ApiClient);
+ apiClient.should.have.property('timeout', config.originClient?.timeout);
+ apiClient.should.have.property('cache', config.originClient?.cache);
+ apiClient.should.have.property('enableCookies', config.originClient?.enableCookies);
+ apiClient.should.have.property('defaultHeaders').and.should.be.a('object');
+ return apiClient.should.and.have.property('basePath', config.originClient?.basePath);
+ });
+
+ it('should return the same object on second call', async (): Promise => {
+ const squareClient: SquareClient = new SquareClient(accessToken);
+ return squareClient.getOriginApiClient().should.be.deep.eq(squareClient.getOriginApiClient());
+ });
+ });
+
+ describe('#getLocationsApi', (): void => {
+ it('should return LocationsApi', async (): Promise => {
+ return new SquareClient(accessToken).getLocationsApi().should.be.instanceOf(LocationsApi);
+ });
+ });
+
+ describe('#getCustomersApi', (): void => {
+ it('should return CustomersApi', async (): Promise => {
+ return new SquareClient(accessToken).getCustomersApi().should.be.instanceOf(CustomersApi);
+ });
+ });
+
+ describe('#getPaymentsApi', (): void => {
+ it('should return PaymentsApi', async (): Promise => {
+ return new SquareClient(accessToken).getPaymentsApi().should.be.instanceOf(PaymentsApi);
+ });
+ });
+
+ describe('#getRefundsApi', (): void => {
+ it('should return RefundsApi', async (): Promise => {
+ return new SquareClient(accessToken).getRefundsApi().should.be.instanceOf(RefundsApi);
+ });
+ });
+
+ describe('#getOrdersApi', (): void => {
+ it('should return OrdersApi', async (): Promise => {
+ return new SquareClient(accessToken).getOrdersApi().should.be.instanceOf(OrdersApi);
+ });
+ });
+});
diff --git a/test/unit/client/square-client.spec.ts b/test/unit/client/square-client.spec.ts
new file mode 100644
index 0000000..f59cd23
--- /dev/null
+++ b/test/unit/client/square-client.spec.ts
@@ -0,0 +1,190 @@
+import {
+ ApiClient,
+ ApplePayApi,
+ CatalogApi,
+ CheckoutApi,
+ CustomersApi,
+ EmployeesApi,
+ InventoryApi,
+ LaborApi,
+ LocationsApi,
+ MobileAuthorizationApi,
+ OAuthApi,
+ OrdersApi,
+ PaymentsApi,
+ RefundsApi,
+ TransactionsApi,
+} from 'square-connect';
+import { SquareClient } from '../../../src/client';
+import { ISquareClientConfig } from '../../../src/interface';
+import { exponentialDelay } from '../../../src/utils';
+
+describe('SquareClient (unit)', (): void => {
+ const accessToken: string = 'test';
+ const basePath: string = `${process.env.SQUARE_SANDBOX_BASE_URL}`;
+
+ const config: ISquareClientConfig = {
+ retry: {
+ maxRetries: 1,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 1000,
+ cache: false,
+ enableCookies: false,
+ basePath,
+ },
+ logger: console,
+ };
+
+ describe('#constructor', (): void => {
+ it('should be init with accessToken only (default config)', async (): Promise => {
+ return new SquareClient(accessToken).should.be.instanceOf(SquareClient);
+ });
+
+ it('should be init with accessToken and config', async (): Promise => {
+ return new SquareClient(accessToken, config).should.be.instanceOf(SquareClient);
+ });
+ });
+
+ describe('#generateIdempotencyKey', (): void => {
+ it('should return string', async (): Promise => {
+ const reference: string = 'test';
+ return SquareClient.generateIdempotencyKey(reference)
+ .should.be.match(new RegExp(`^${reference}-.*`))
+ .and.lengthOf(18);
+ });
+ });
+
+ describe('#getConfig', (): void => {
+ it('should return default configuration', async (): Promise => {
+ return new SquareClient(accessToken).getConfig().should.be.deep.eq({
+ retry: {
+ maxRetries: 6,
+ retryDelay: exponentialDelay,
+ },
+ originClient: {
+ timeout: 15000,
+ },
+ logger: undefined,
+ });
+ });
+
+ it('should return custom configuration', async (): Promise => {
+ return new SquareClient(accessToken, config).getConfig().should.be.deep.eq(config);
+ });
+ });
+
+ describe('#getOriginApiClient', (): void => {
+ it('should return ApiClient with default configuration', async (): Promise => {
+ const apiClient: ApiClient = new SquareClient(accessToken).getOriginApiClient();
+
+ apiClient.should.be.instanceOf(ApiClient);
+ apiClient.should.have.property('timeout', 15000);
+ apiClient.should.have.property('cache', true);
+ apiClient.should.have.property('enableCookies', false);
+ apiClient.should.have.property('defaultHeaders').and.should.be.a('object');
+ return apiClient.should.and.have.property('basePath', 'https://connect.squareup.com');
+ });
+
+ it('should return ApiClient with custom configuration', async (): Promise => {
+ const apiClient: ApiClient = new SquareClient(accessToken, config).getOriginApiClient();
+
+ apiClient.should.be.instanceOf(ApiClient);
+ apiClient.should.have.property('timeout', config.originClient?.timeout);
+ apiClient.should.have.property('cache', config.originClient?.cache);
+ apiClient.should.have.property('enableCookies', config.originClient?.enableCookies);
+ apiClient.should.have.property('defaultHeaders').and.should.be.a('object');
+ return apiClient.should.and.have.property('basePath', config.originClient?.basePath);
+ });
+
+ it('should return the same object on second call', async (): Promise => {
+ const squareClient: SquareClient = new SquareClient(accessToken);
+ return squareClient.getOriginApiClient().should.be.deep.eq(squareClient.getOriginApiClient());
+ });
+ });
+
+ describe('#getApplePayApi', (): void => {
+ it('should return ApplePayApi', async (): Promise => {
+ return new SquareClient(accessToken).getApplePayApi().should.be.instanceOf(ApplePayApi);
+ });
+ });
+
+ describe('#getCatalogApi', (): void => {
+ it('should return CatalogApi', async (): Promise => {
+ return new SquareClient(accessToken).getCatalogApi().should.be.instanceOf(CatalogApi);
+ });
+ });
+
+ describe('#getCheckoutApi', (): void => {
+ it('should return CheckoutApi', async (): Promise => {
+ return new SquareClient(accessToken).getCheckoutApi().should.be.instanceOf(CheckoutApi);
+ });
+ });
+
+ describe('#getCustomersApi', (): void => {
+ it('should return CustomersApi', async (): Promise => {
+ return new SquareClient(accessToken).getCustomersApi().should.be.instanceOf(CustomersApi);
+ });
+ });
+
+ describe('#getEmployeesApi', (): void => {
+ it('should return EmployeesApi', async (): Promise => {
+ return new SquareClient(accessToken).getEmployeesApi().should.be.instanceOf(EmployeesApi);
+ });
+ });
+
+ describe('#getInventoryApi', (): void => {
+ it('should return InventoryApi', async (): Promise => {
+ return new SquareClient(accessToken).getInventoryApi().should.be.instanceOf(InventoryApi);
+ });
+ });
+
+ describe('#getLaborApi', (): void => {
+ it('should return LaborApi', async (): Promise => {
+ return new SquareClient(accessToken).getLaborApi().should.be.instanceOf(LaborApi);
+ });
+ });
+
+ describe('#getLocationsApi', (): void => {
+ it('should return LocationsApi', async (): Promise => {
+ return new SquareClient(accessToken).getLocationsApi().should.be.instanceOf(LocationsApi);
+ });
+ });
+
+ describe('#getMobileAuthorizationApi', (): void => {
+ it('should return MobileAuthorizationApi', async (): Promise => {
+ return new SquareClient(accessToken).getMobileAuthorizationApi().should.be.instanceOf(MobileAuthorizationApi);
+ });
+ });
+
+ describe('#getOAuthApi', (): void => {
+ it('should return OAuthApi', async (): Promise => {
+ return new SquareClient(accessToken).getOAuthApi().should.be.instanceOf(OAuthApi);
+ });
+ });
+
+ describe('#getOrdersApi', (): void => {
+ it('should return OrdersApi', async (): Promise => {
+ return new SquareClient(accessToken).getOrdersApi().should.be.instanceOf(OrdersApi);
+ });
+ });
+
+ describe('#getPaymentsApi', (): void => {
+ it('should return PaymentsApi', async (): Promise => {
+ return new SquareClient(accessToken).getPaymentsApi().should.be.instanceOf(PaymentsApi);
+ });
+ });
+
+ describe('#getRefundsApi', (): void => {
+ it('should return RefundsApi', async (): Promise => {
+ return new SquareClient(accessToken).getRefundsApi().should.be.instanceOf(RefundsApi);
+ });
+ });
+
+ describe('#getTransactionsApi', (): void => {
+ it('should return TransactionsApi', async (): Promise => {
+ return new SquareClient(accessToken).getTransactionsApi().should.be.instanceOf(TransactionsApi);
+ });
+ });
+});
diff --git a/test/unit/exception/square-exception.spec.ts b/test/unit/exception/square-exception.spec.ts
new file mode 100644
index 0000000..c87088f
--- /dev/null
+++ b/test/unit/exception/square-exception.spec.ts
@@ -0,0 +1,195 @@
+import { SquareException } from '../../../src/exception';
+
+class SuperAgentError extends Error {
+ constructor(message: string, readonly response?: any, readonly code?: string) {
+ super(message);
+ }
+}
+
+describe('SquareException (unit)', (): void => {
+ describe('#constructor', (): void => {
+ it('should be ok without args', async (): Promise => {
+ const squareException: SquareException = new SquareException();
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 0);
+ squareException.should.have.property('statusCode', 500);
+ return squareException.should.have.property('apiError').and.eql({ category: 'API_ERROR', code: 'SERVICE_UNAVAILABLE', detail: 'Square API error' });
+ });
+
+ it('should be ok with empty data.originError object', async (): Promise => {
+ // @ts-ignore
+ const squareException: SquareException = new SquareException({ apiError: {} });
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 0);
+ squareException.should.have.property('statusCode', 500);
+ return squareException.should.have.property('apiError').and.eql({});
+ });
+
+ it('should be ok with empty data.originError object and originError', async (): Promise => {
+ // @ts-ignore
+ const squareException: SquareException = new SquareException({ apiError: {} }, new Error('error message'));
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'error message');
+ squareException.should.have.property('retries', 0);
+ squareException.should.have.property('statusCode', 500);
+ return squareException.should.have.property('apiError').and.eql({});
+ });
+ });
+
+ describe('#createFromSuperAgentError', (): void => {
+ it('should be ok with Error', async (): Promise => {
+ const error: Error = new Error('test message');
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 0);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 0);
+ squareException.should.have.property('statusCode', 500);
+ return squareException.should.have.property('apiError').and.eql({ category: 'API_ERROR', code: 'SERVICE_UNAVAILABLE', detail: 'Square API error' });
+ });
+
+ it('should be ok with Error with ECONNABORTED error code', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', undefined, 'ECONNABORTED');
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API timeout');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 500);
+ squareException.should.have.property('url', undefined);
+ squareException.should.have.property('method', undefined);
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'API_ERROR',
+ code: 'GATEWAY_TIMEOUT',
+ detail: 'Square API timeout',
+ });
+ });
+
+ it('should be ok with Error with empty response object', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', {});
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 500);
+ squareException.should.have.property('url', undefined);
+ squareException.should.have.property('method', undefined);
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'API_ERROR',
+ code: 'SERVICE_UNAVAILABLE',
+ detail: 'Square API error',
+ });
+ });
+
+ it('should be ok with Error with response status', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', {
+ status: 429,
+ });
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 429);
+ squareException.should.have.property('url', undefined);
+ squareException.should.have.property('method', undefined);
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'API_ERROR',
+ code: 'SERVICE_UNAVAILABLE',
+ detail: 'Square API error',
+ });
+ });
+
+ it('should be ok with Error with response body', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', {
+ status: 429,
+ body: {},
+ });
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'Square API error');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 429);
+ squareException.should.have.property('url', undefined);
+ squareException.should.have.property('method', undefined);
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'API_ERROR',
+ code: 'SERVICE_UNAVAILABLE',
+ detail: 'Square API error',
+ });
+ });
+
+ it('should be ok with Error with response body.errors', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', {
+ status: 429,
+ body: {
+ errors: [
+ {
+ category: 'RATE_LIMIT_ERROR',
+ code: 'RATE_LIMITED',
+ detail: 'fake 429 error',
+ },
+ ],
+ },
+ });
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'fake 429 error');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 429);
+ squareException.should.have.property('url', undefined);
+ squareException.should.have.property('method', undefined);
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'RATE_LIMIT_ERROR',
+ code: 'RATE_LIMITED',
+ detail: 'fake 429 error',
+ });
+ });
+
+ it('should be ok with Error + response and request properties ', async (): Promise => {
+ const error: SuperAgentError = new SuperAgentError('test message', {
+ status: 429,
+ body: {
+ errors: [
+ {
+ category: 'RATE_LIMIT_ERROR',
+ code: 'RATE_LIMITED',
+ detail: 'fake 429 error',
+ },
+ ],
+ },
+ request: {
+ url: 'url',
+ method: 'get',
+ },
+ });
+
+ const squareException: SquareException = SquareException.createFromSuperAgentError(error, 1);
+
+ squareException.should.be.instanceOf(SquareException);
+ squareException.should.have.property('message', 'fake 429 error');
+ squareException.should.have.property('retries', 1);
+ squareException.should.have.property('statusCode', 429);
+ squareException.should.have.property('url', 'url');
+ squareException.should.have.property('method', 'GET');
+ return squareException.should.have.property('apiError').and.eql({
+ category: 'RATE_LIMIT_ERROR',
+ code: 'RATE_LIMITED',
+ detail: 'fake 429 error',
+ });
+ });
+ });
+});
diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts
new file mode 100644
index 0000000..6faa584
--- /dev/null
+++ b/test/unit/index.spec.ts
@@ -0,0 +1 @@
+export * from '../../src';
diff --git a/test/unit/logger/null-logger.spec.ts b/test/unit/logger/null-logger.spec.ts
new file mode 100644
index 0000000..2ac01b2
--- /dev/null
+++ b/test/unit/logger/null-logger.spec.ts
@@ -0,0 +1,77 @@
+import { ILogger, NullLogger } from '../../../src/logger';
+
+describe('NullLogger (unit)', (): void => {
+ describe('#constructor', (): void => {
+ it('should be init without args', async (): Promise => {
+ return new NullLogger().should.be.instanceOf(NullLogger);
+ });
+ });
+
+ describe('#debug', (): void => {
+ it('should have debug method', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ return logger.debug.should.be.a('function');
+ });
+
+ it('should be ok', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ try {
+ logger.debug('test');
+ return true.should.be.true;
+ } catch (e) {
+ return true.should.be.false;
+ }
+ });
+ });
+
+ describe('#info', (): void => {
+ it('should have debug method', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ return logger.info.should.be.a('function');
+ });
+
+ it('should be ok', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ try {
+ logger.info('test');
+ return true.should.be.true;
+ } catch (e) {
+ return true.should.be.false;
+ }
+ });
+ });
+
+ describe('#warn', (): void => {
+ it('should have warn method', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ return logger.warn.should.be.a('function');
+ });
+
+ it('should be ok', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ try {
+ logger.warn('test');
+ return true.should.be.true;
+ } catch (e) {
+ return true.should.be.false;
+ }
+ });
+ });
+
+ describe('#error', (): void => {
+ it('should have error method', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ return logger.error.should.be.a('function');
+ });
+
+ it('should be ok', async (): Promise => {
+ const logger: ILogger = new NullLogger();
+ try {
+ logger.error('test');
+ return true.should.be.true;
+ } catch (e) {
+ return true.should.be.false;
+ }
+ });
+ });
+});
diff --git a/test/unit/utils/common.utils.spec.ts b/test/unit/utils/common.utils.spec.ts
new file mode 100644
index 0000000..8bfdedb
--- /dev/null
+++ b/test/unit/utils/common.utils.spec.ts
@@ -0,0 +1,38 @@
+import { mergeDeepProps } from '../../../src/utils';
+
+describe('common.utils (unit)', (): void => {
+ describe('#mergeDeepProps', (): void => {
+ it('should correctly merge simple object with undefined', async (): Promise => {
+ return mergeDeepProps({ a: 1 }, undefined as any).should.be.deep.eq({ a: 1 });
+ });
+
+ it('should correctly merge class props with undefined', async (): Promise => {
+ const error: Error = new Error();
+ return mergeDeepProps(error, undefined as any)
+ .should.be.instanceOf(Error)
+ .and.deep.eq(error)
+ .and.have.property('name', 'Error');
+ });
+
+ it('should correctly merge 2 empty objects', async (): Promise => {
+ return mergeDeepProps({}, {}).should.be.deep.eq({});
+ });
+
+ it('should correctly merge 2 simple object props', async (): Promise => {
+ return mergeDeepProps({ a: 1, b: 1, c: 1 }, { b: 2, c: 2, e: 2 }).should.be.deep.eq({ a: 1, b: 2, c: 2, e: 2 });
+ });
+
+ it('should correctly merge 3 simple objects props', async (): Promise => {
+ return mergeDeepProps({ a: 1, b: 1, c: 1 }, { b: 2, c: 2, e: 2 }, { b: 3, c: 3, d: 3 }).should.be.deep.eq({ a: 1, b: 3, c: 3, e: 2, d: 3 });
+ });
+
+ it('should correctly merge 2 complex object props', async (): Promise => {
+ return mergeDeepProps({ a: { b: 1 }, c: 1, d: 1 }, { a: { b: 2 }, c: 2, e: 2 }).should.be.deep.eq({ a: { b: 2 }, c: 2, d: 1, e: 2 });
+ });
+
+ it('should correctly merge class props with simple object', async (): Promise => {
+ const error: Error = new Error();
+ return mergeDeepProps(error, { name: 'x' }).should.be.instanceOf(Error).and.deep.eq(error).and.have.property('name', 'x');
+ });
+ });
+});
diff --git a/test/unit/utils/retry.utils.spec.ts b/test/unit/utils/retry.utils.spec.ts
new file mode 100644
index 0000000..b1596d8
--- /dev/null
+++ b/test/unit/utils/retry.utils.spec.ts
@@ -0,0 +1,38 @@
+import { exponentialDelay, isRetryableException, makeRetryable } from '../../../src/utils';
+
+describe('retry.utils (unit)', (): void => {
+ describe('#exponentialDelay', (): void => {
+ it('should return >= 200 and <= 250', async (): Promise => {
+ return exponentialDelay(1).should.be.gte(200).and.lte(240);
+ });
+
+ it('should return >= 400 and <= 500', async (): Promise => {
+ return exponentialDelay(2).should.be.gte(400).and.lte(480);
+ });
+
+ it('should return >= 800 and <= 1200', async (): Promise => {
+ return exponentialDelay(3).should.be.gte(800).and.lte(1000);
+ });
+ });
+
+ describe('#makeRetryable', (): void => {
+ it('should throw SquareException', async (): Promise => {
+ return makeRetryable(
+ async (): Promise => {
+ throw new Error('test error');
+ },
+ {
+ maxRetries: 1,
+ retryDelay: exponentialDelay,
+ retryCondition: async (_error: Error, maxRetries: number, retries: number): Promise => maxRetries > retries,
+ },
+ ).should.be.rejectedWith(Error, 'Square API error');
+ });
+ });
+
+ describe('#isRetryableException', (): void => {
+ it('should return false if Error is not instanceof SquareException', async (): Promise => {
+ return isRetryableException(new Error()).should.be.eq(false);
+ });
+ });
+});
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 0000000..2653248
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "sourceMap": true,
+ "incremental": false
+ },
+ "include": ["src"],
+ "exclude": ["src/test.ts"]
+}
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
new file mode 100644
index 0000000..3004417
--- /dev/null
+++ b/tsconfig.eslint.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*", "test/**/*", "bin/**/*", "*.json"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..89fdc51
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "esModuleInterop": true,
+ "target": "es6",
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "strictNullChecks": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": false,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "sourceMap": false,
+ "declaration": false,
+ "noImplicitAny": false,
+ "removeComments": false,
+ "noLib": false,
+ "allowSyntheticDefaultImports": true,
+ "outDir": "dist",
+ "baseUrl": "src",
+ "lib": ["es2017"]
+ },
+ "include": ["src", "test"]
+}