commit ce4ff7eac5e253dc0e4a1fabaa0997a02699bca3 Author: Augustine Rapheal Date: Sat Jul 13 14:33:19 2024 +0100 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9a874b5 Binary files /dev/null and b/.DS_Store differ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb38189 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +MYSQL_DATABASE= +MYSQL_USERNAME= +MYSQL_ROOT_PASSWORD= +MYSQL_HOST= +MYSQL_DOCKER_PORT= + +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASS= +EMAIL_FROM= + +ACCESS_TOKEN_PRIVATE_KEY= +REFRESH_TOKEN_PRIVATE_KEY= + +ZOHO_CLIENT_ID= +ZOHO_CLIENT_SECRET= +ZOHO_CLIENT_REDIRECT_URI= +ZOHO_BASE_URL= +ZOHO_BOOK_BASE_URL= + +ORGANIZATION_ID= +DOLLAR_CURRENCY_ID= + +BASE_URL= +BASE_TEST_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fdd251 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.development + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e80a97 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# financial-forecast diff --git a/config.js b/config.js new file mode 100644 index 0000000..d86c476 --- /dev/null +++ b/config.js @@ -0,0 +1,36 @@ +require('dotenv').config( + process.env.NODE_ENV === 'development' ? { path: `.env.development` } : null +); + +module.exports = { + NODE_ENV: process.env.NODE_ENV, + HOST: process.env.HOST, + PORT: process.env.PORT, + MYSQL_DATABASE: process.env.MYSQL_DATABASE, + MYSQL_USERNAME: process.env.MYSQL_USERNAME, + MYSQL_ROOT_PASSWORD: process.env.MYSQL_ROOT_PASSWORD, + MYSQL_HOST: process.env.MYSQL_HOST, + MYSQL_DOCKER_PORT: process.env.DOCKER_PORT, + + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + EMAIL_FROM: process.env.EMAIL_FROM, + + ACCESS_TOKEN_PRIVATE_KEY: process.env.ACCESS_TOKEN_PRIVATE_KEY, + REFRESH_TOKEN_PRIVATE_KEY: process.env.REFRESH_TOKEN_PRIVATE_KEY, + + ZOHO_CLIENT_ID: process.env.ZOHO_CLIENT_ID, + ZOHO_CLIENT_SECRET: process.env.ZOHO_CLIENT_SECRET, + ZOHO_BASE_URL: process.env.ZOHO_BASE_URL, + ZOHO_BOOK_BASE_URL: process.env.ZOHO_BOOK_BASE_URL, + ZOHO_REFRESH_TOKEN: process.env.ZOHO_REFRESH_TOKEN, + ZOHO_REDIRECT_URI: process.env.ZOHO_REDIRECT_URI, + + ORGANIZATION_ID: process.env.ORGANIZATION_ID, + DOLLAR_CURRENCY_ID: process.env.DOLLAR_CURRENCY_ID, + + BASE_URL: process.env.BASE_URL, + BASE_TEST_URL: process.env.BASE_TEST_URL, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a2d1432 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2858 @@ +{ + "name": "manifold-financial-forecast", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "manifold-financial-forecast", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/auth": "^3.0.2", + "@fastify/cors": "^8.0.0", + "@fastify/formbody": "^7.0.1", + "axios": "^0.27.2", + "bcrypt": "^5.0.1", + "crypto-js": "^4.1.1", + "dayjs": "^1.11.4", + "dotenv": "^16.0.1", + "dotenv-load": "^2.0.1", + "exceljs": "^4.3.0", + "fastify": "^4.2.0", + "fastify-axios": "^1.2.5", + "handlebars": "^4.7.7", + "jsonwebtoken": "^8.5.1", + "moment": "^2.29.4", + "mysql2": "^2.3.3", + "nodemailer": "^6.7.7", + "nodemon": "^2.0.19", + "path": "^0.12.7", + "sequelize": "^6.21.3", + "uuid": "^8.3.2" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/auth": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/auth/-/auth-3.0.2.tgz", + "integrity": "sha512-71Np39NhAK8YCY3SOq3ELswVgF2bpGNnIj494pdJ3ffiCoHChKggY8kgfBUsim+VVOiBxHKpkxcJNq4iAtILSQ==", + "dependencies": { + "fastify-plugin": "^3.0.0", + "reusify": "^1.0.4" + } + }, + "node_modules/@fastify/cors": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", + "integrity": "sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/cors/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/formbody": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.4.0.tgz", + "integrity": "sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==", + "dependencies": { + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/formbody/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", + "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/dotenv-load": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dotenv-load/-/dotenv-load-2.0.1.tgz", + "integrity": "sha512-MNuXfspporu/oj1UeQuf88Zm7WjkUgGRH4z0pGyVqm6B924v+CdfQLzusyux+bvOssK7p+0ULYXn764pnGVRSA==", + "dependencies": { + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.2", + "shelljs": "^0.8.5" + }, + "bin": { + "dotenv-load": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.13.0.tgz", + "integrity": "sha512-XjTDWKHP3GoMQUOfnjYUbqeHeEt+PvYgvBdG2fRSmYaORILbSr8xTJvZX+w1YSAP5pw2NwKrGRmQleYueZEoxw==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.4.0.tgz", + "integrity": "sha512-2gwPvyna0zwBdxKnng1suu/dTL5s8XEy2ZqH8mwDUwJdDkV8w5kp+JV26mupdK68HmPMbm6yjW9m7/Ys/BHEHg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.2.tgz", + "integrity": "sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-axios": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/fastify-axios/-/fastify-axios-1.2.8.tgz", + "integrity": "sha512-9/y36e5ZdoGz/YEWfKultjma9az5Xt5iwN+ESfNvfDlq7nUvf9F4UU+MCkF9BZhCbcgUl/9yKJNS5dWRvAG2Ag==", + "dependencies": { + "axios": "^1.6.2", + "fastify-plugin": "^4.5.1" + } + }, + "node_modules/fastify-axios/node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/fastify-axios/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/fastify-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz", + "integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/light-my-request": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.12.0.tgz", + "integrity": "sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mysql2": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", + "dependencies": { + "denque": "^2.0.1", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemailer": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/sequelize": { + "version": "6.37.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.1.tgz", + "integrity": "sha512-vIKKzQ9dGp2aBOxQRD1FmUYViuQiKXSJ8yah8TsaBx4U3BokJt+Y2A0qz2C4pj08uX59qpWxRqSLEfRmVOEgQw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..577401d --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "manifold-financial-forecast", + "version": "1.0.0", + "description": "an application for predictive cash flow within a given period", + "main": "server.js", + "scripts": { + "start": "NODE_ENV=production node server.js", + "dev": "NODE_ENV=development nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/manifoldcomputers/financial-forecast.git" + }, + "author": "Liassidji Abiodun Manasseh", + "license": "ISC", + "bugs": { + "url": "https://github.com/manifoldcomputers/financial-forecast/issues" + }, + "homepage": "https://github.com/manifoldcomputers/financial-forecast#readme", + "dependencies": { + "@fastify/auth": "^3.0.2", + "@fastify/cors": "^8.0.0", + "@fastify/formbody": "^7.0.1", + "axios": "^0.27.2", + "bcrypt": "^5.0.1", + "crypto-js": "^4.1.1", + "dayjs": "^1.11.4", + "dotenv": "^16.0.1", + "dotenv-load": "^2.0.1", + "exceljs": "^4.3.0", + "fastify": "^4.2.0", + "fastify-axios": "^1.2.5", + "handlebars": "^4.7.7", + "jsonwebtoken": "^8.5.1", + "moment": "^2.29.4", + "mysql2": "^2.3.3", + "nodemailer": "^6.7.7", + "nodemon": "^2.0.19", + "path": "^0.12.7", + "sequelize": "^6.21.3", + "uuid": "^8.3.2" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..84900a8 --- /dev/null +++ b/server.js @@ -0,0 +1,60 @@ +//load fastify itself +const app = require('fastify')({ + logger: true, + trustProxy: true, +}); +const config = require('./config'); + +const cors = require('@fastify/cors'); +const fastifyFormbody = require('@fastify/formbody'); +const fastifyAxios = require('fastify-axios'); + +// load fastify plugins +app + .register(cors, { + origin: '*', + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], + }) + .register(fastifyFormbody) + .register(fastifyAxios, { + clients: { + zoho: { + baseURL: 'https://www.zohoapis.com/crm/v2/', + // headers: { + // Authorization: `Zoho-oauthtoken ${process.env.ZOHO_TOKEN}`, + // "Content-Type": "application/json", + // }, + }, + }, + }); + +const db = require('./src/models'); +db.sequelize.sync(); + +app.register(require('./src/routes/auth.routes.js'), { prefix: 'v1' }); +app.register(require('./src/routes/zoho.routes.js'), { prefix: 'v1' }); + +// Declare a route +app.register( + function (instance, options, next) { + instance.setNotFoundHandler(function (request, reply) { + reply.code(404).send({ message: "You're in the wrong place" }); + }); + next(); + }, + { prefix: '/' } +); + +// Run the server! +const start = async () => { + try { + const PORT = parseInt(config.PORT); + await app.listen({ port: 4000 }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +}; +start(); + +exports.default = app; diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..4ac366d Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/controllers/.DS_Store b/src/controllers/.DS_Store new file mode 100644 index 0000000..c21ee72 Binary files /dev/null and b/src/controllers/.DS_Store differ diff --git a/src/controllers/auth/generateToken.js b/src/controllers/auth/generateToken.js new file mode 100644 index 0000000..6df1a4b --- /dev/null +++ b/src/controllers/auth/generateToken.js @@ -0,0 +1,31 @@ +const jwt = require('jsonwebtoken'); +const db = require('../../models'); +const User = db.users; +const config = require('../../../config'); + +const generateTokens = async (user) => { + let refreshToken = user.token; + delete user.token; + let payload = { + ...user, + }; + + try { + const accessToken = jwt.sign(payload, config.ACCESS_TOKEN_PRIVATE_KEY, { + expiresIn: '1d', + }); + + if (refreshToken === null) { + refreshToken = jwt.sign(payload, config.REFRESH_TOKEN_PRIVATE_KEY, {}); + const user = await User.findOne({ where: { email: payload.email } }); + + await user.update({ token: refreshToken }); + } + + return Promise.resolve({ accessToken, refreshToken }); + } catch (e) { + return Promise.reject(e); + } +}; + +module.exports = { generateTokens }; diff --git a/src/controllers/auth/verifyRefreshToken.js b/src/controllers/auth/verifyRefreshToken.js new file mode 100644 index 0000000..9fdad1c --- /dev/null +++ b/src/controllers/auth/verifyRefreshToken.js @@ -0,0 +1,37 @@ +const jwt = require('jsonwebtoken'); +const db = require('../../models'); +const config = require('../../../config'); +const User = db.users; + +const verifyRefreshToken = (refreshToken, reply) => { + const privateKey = config.REFRESH_TOKEN_PRIVATE_KEY; + + return new Promise((resolve, reject) => { + const user = User.findOne({ + where: { token: refreshToken }, + }); + + if (!user) + return reply.code(500).send({ + statusCode: 500, + message: 'Invalid refresh token', + error: true, + }); + + jwt.verify(refreshToken, privateKey, (err, tokenDetails) => { + if (err) + return reply.code(500).send({ + statusCode: 500, + message: 'Invalid refresh token', + error: true, + }); + resolve({ + tokenDetails, + error: false, + message: 'Valid refresh token', + }); + }); + }); +}; + +module.exports = { verifyRefreshToken }; diff --git a/src/controllers/auth/verifyToken.js b/src/controllers/auth/verifyToken.js new file mode 100644 index 0000000..ffe9914 --- /dev/null +++ b/src/controllers/auth/verifyToken.js @@ -0,0 +1,25 @@ +const jwt = require('jsonwebtoken'); +const db = require('../../models'); +const config = require('../../../config'); +const User = db.users; + +const verifyToken = async (req, reply, done) => { + const token = req?.headers?.authorization?.split(' ')[1]; + + const decoded = jwt.verify(token, config.ACCESS_TOKEN_PRIVATE_KEY); + + if (!decoded) { + throw new Error('Authorization failed'); + } + + let user = await User.findOne({ + where: { email: decoded.email }, + }); + if (!user) { + throw new Error('Authentication failed'); + } + + req.user = user.dataValues; +}; + +module.exports = verifyToken; diff --git a/src/controllers/handlers/.DS_Store b/src/controllers/handlers/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/controllers/handlers/.DS_Store differ diff --git a/src/controllers/handlers/auth.handler.js b/src/controllers/handlers/auth.handler.js new file mode 100644 index 0000000..1458599 --- /dev/null +++ b/src/controllers/handlers/auth.handler.js @@ -0,0 +1,797 @@ +let CryptoJS = require('crypto-js'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { v4: uuidv4 } = require('uuid'); +const { sendEmail } = require('../../helpers/email'); +const { generateTokens } = require('../auth/generateToken'); +const { verifyRefreshToken } = require('../auth/verifyRefreshToken'); +const config = require('../../../config'); + +const db = require('../../models'); +const User = db.users; +const Accruedexp = db.accruedexp; +const Prepaidexp = db.prepaidexp; + + +async function findUserByEmailStatus(email, status, raw = true) { + return await User.findOne({ where: { email, status }, raw: raw }); +} + +async function findUserByEmail(email) { + return await User.findOne({ where: { email }, raw: true }); +} + +const savePrepaidPaymentHandler = async (req, reply) => { + const { account_id, tax_id, payment_date, amount, reference_number, customer_id, description, currency_id, currency } = req.body; + let statusCode; + let result; + try { + let prepaidexp; + let paymentDate = new Date(payment_date); + + const formattedDate = paymentDate.toISOString().slice(0, 10); + + prepaidexp = await Prepaidexp.create({ + account_id: account_id, + tax_id: tax_id, + payment_date: formattedDate, + amount: amount, + reference_number: reference_number, + customer_id: customer_id, + description: description, + currency_id: currency_id, + currency: currency, + }); + + statusCode = 200; + + result = { + status: true, + message: 'Prepare expense inserted successfully', + }; + } catch (e) { + statusCode = e.code || 500; + result = { + status: false, + message: e.message || 'Internal Server Error', + }; + } + return reply.status(statusCode).send(result); +}; +const updatePrepaidPaymentHandler = async (req, reply) => { + const { id, paymentConfirmation, account_id, tax_id, payment_date, amount, reference_number, customer_id, description, currency_id, currency } = req.body; + let statusCode; + let result; + try { + // Fetch the existing accrued expense by its ID + let accruedExpense = await Accruedexp.findByPk(id); + if (!accruedExpense) { + statusCode = 404; + result = { + status: false, + message: 'Accrued expense not found', + }; + return reply.status(statusCode).send(result); + } + + // Update the properties with the provided values + accruedExpense.account_id = account_id; + accruedExpense.tax_id = tax_id; + accruedExpense.payment_date = payment_date; + accruedExpense.amount = amount; + accruedExpense.reference_number = reference_number; + accruedExpense.customer_id = customer_id; + accruedExpense.description = description; + accruedExpense.currency_id = currency_id; + accruedExpense.currency = currency; + accruedExpense.paymentConfirmation = paymentConfirmation; + + // Save the updated accrued expense + await accruedExpense.save(); + + statusCode = 200; + result = { + status: true, + message: 'Accrued expense updated successfully', + }; + } catch (e) { + statusCode = e.code || 500; + result = { + status: false, + message: e.message || 'Internal Server Error', + }; + } + return reply.status(statusCode).send(result); +}; + +const updateAccruedPaymentHandler = async (req, reply) => { + const { id, paymentConfirmation, account_id, tax_id, payment_date, amount, reference_number, customer_id, description, currency_id, currency } = req.body; + let statusCode; + let result; + try { + // Fetch the existing accrued expense by its ID + let accruedExpense = await Accruedexp.findByPk(id); + if (!accruedExpense) { + statusCode = 404; + result = { + status: false, + message: 'Accrued expense not found', + }; + return reply.status(statusCode).send(result); + } + + // Update the properties with the provided values + accruedExpense.account_id = account_id; + accruedExpense.tax_id = tax_id; + accruedExpense.payment_date = payment_date; + accruedExpense.amount = amount; + accruedExpense.reference_number = reference_number; + accruedExpense.customer_id = customer_id; + accruedExpense.description = description; + accruedExpense.currency_id = currency_id; + accruedExpense.currency = currency; + accruedExpense.paymentConfirmation = paymentConfirmation; + + // Save the updated accrued expense + await accruedExpense.save(); + + statusCode = 200; + result = { + status: true, + message: 'Accrued expense updated successfully', + }; + } catch (e) { + statusCode = e.code || 500; + result = { + status: false, + message: e.message || 'Internal Server Error', + }; + } + return reply.status(statusCode).send(result); +}; +const saveAccruedPaymentHandler = async (req, reply) => { + const { account_id, tax_id, payment_date, amount, reference_number, customer_id, description, currency_id, currency } = req.body; + let statusCode; + let result; + try { + + let prepaidexp; + let paymentDate = new Date(payment_date); + const dateObject = new Date(paymentDate); + const year = dateObject.getFullYear(); + const month = String(dateObject.getMonth() + 1).padStart(2, '0'); // Adding 1 because months are zero-based + const day = String(dateObject.getDate()).padStart(2, '0'); + const formattedDate = `${year}-${month}-${day}`; + console.log("O>>>>>>>>>>>>>>>>>>>>>.",formattedDate) + + prepaidexp = await Accruedexp.create({ + account_id: account_id, + tax_id: tax_id, + payment_date: formattedDate, + amount: amount, + reference_number: reference_number, + customer_id: customer_id, + description: description, + currency_id: currency_id, + currency: currency, + }); + + statusCode = 200; +console.log(prepaidexp,">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + result = { + status: true, + message: 'Accrued expense inserted successfully', + }; + } catch (e) { + statusCode = e.code || 500; + result = { + status: false, + message: e.message || 'Internal Server Error', + }; + } + return reply.status(statusCode).send(result); +} + +/** + * invite or re-invite a user + * by default user role is 'user' + * @param {string} email - the email of the user. + * @returns {object} - information about the user. + */ +const inviteUserHandler = async (req, reply) => { + const { email } = req.body; + const status = false; + + try { + let user; + // check if email doesn't already exist + let userExists = await findUserByEmail(email); + + // check if user has already registered + if (userExists && userExists.status) + return reply.code(409).send({ + status: false, + message: 'User already exist', + }); + + // generate a unique token for the user + let token = uuidv4(); + let ciphertext = CryptoJS.AES.encrypt(token, 'ManifoldSecret').toString(); + + // remove special characters to the token + let updatedCipherText = ciphertext + .toString() + .replaceAll('+', 'xMl3Jk') + .replaceAll('/', 'Por21Ld') + .replaceAll('=', 'Ml32'); + + // invite a new user + if (!userExists) { + // store the data in the database + user = await User.create({ + email, + inviteToken: token, + inviteDate: new Date(), + }); + } + + // re-invite a user + if (userExists && !userExists.status) { + // update the user's status to false + user = await User.update( + { + inviteToken: token, + inviteDate: new Date(), + }, + { where: { email } } + ); + } + + const details = { + name: 'User', + templateToUse: 'invite', + url: `${config.BASE_URL}/register/${updatedCipherText}`, + }; + + console.log(details) + // invite user by sending an email + await sendEmail( + email, + 'Manifold Forecast Invitation', + 'Please click the link below to complete registration', + details + ); + console.log(sendEmail) + statusCode = 200; + + result = { + status: true, + message: 'Users invited successfully', + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +/** + * login a user + * @param {string} email - the email of the user. + * @param {string} password - the password of the user. + * @returns {object} - information about the user. + */ +const loginUserHandler = async (req, reply) => { + const { email, password } = req.body; + const status = true; + + try { + // check if email exists + let user = await findUserByEmailStatus(email, status); + + if (!user) + return reply.code(500).send({ + status: false, + message: 'Invalid email or password', + }); + + // check if password matches + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) + return reply.code(401).send({ + status: false, + message: 'Invalid email or password', + }); + + // generate tokens + const { accessToken, refreshToken } = await generateTokens(user); + + statusCode = 200; + + result = { + status: true, + message: 'User logged in successfully', + data: { + accessToken, + refreshToken, + isZohoAuthenticated: user.isZohoAuthenticated ? true : false, + }, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +/** + * register a user + * @param {string} firstName - the first name of the user. + * @param {string} lastName - the last name of the user. + * @param {string} email - the email of the user. + * @param {string} password - the password of the user. + * @param {object} inviteToken - the token generated when an invite is sent to a user. + * @returns {object} - information about the user. + */ +const registerUserHandler = async (req, reply) => { + const { firstName, lastName, email, password, inviteToken } = req.body; + + try { + // TODO:: check if link has expired + // add special characters to the token + let updatedToken = inviteToken + .toString() + .replaceAll('xMl3Jk', '+') + .replaceAll('Por21Ld', '/') + .replaceAll('Ml32', '='); + + // decrypt the invitation token + let bytes = CryptoJS.AES.decrypt(updatedToken, 'ManifoldSecret'); + let originalText = bytes.toString(CryptoJS.enc.Utf8); + + // Hash the password + const hashedPassword = await bcrypt.hash(password, 10); + + // check if email and token match the database + let user = await User.findOne({ + where: { email, inviteToken: originalText }, + }); + + // check if user has been invited + if (!user) + return reply.code(401).send({ + status: false, + message: 'Invalid email or token', + }); + + // check if user has already completed registration + if (user.status) + return reply.code(409).send({ + status: false, + message: 'Already registered', + }); + + // update the user's status to true + await User.update( + { + firstName, + lastName, + status: true, + password: hashedPassword, + }, + { where: { email } } + ); + + // send an email to the user to notify them of their registration + const details = { + name: `${firstName} ${lastName}`, + templateToUse: 'signup', + url: `${config.BASE_URL}`, + }; + + sendEmail( + email, + 'Manifold Forecast Signup', + 'Registration completed', + details + ); + + statusCode = 201; + + result = { + status: true, + message: 'User registered successfully', + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +// A user who wants to refresh token +const refreshTokenHandler = async (req, reply) => { + try { + const { refreshToken } = req.body; + const { tokenDetails } = await verifyRefreshToken(refreshToken, reply); + + if (!tokenDetails) { + return reply.code(401).send({ + status: false, + message: 'Invalid email or password', + }); + } + + delete tokenDetails.exp; + delete tokenDetails.iat; + + const accessToken = jwt.sign( + tokenDetails, + config.ACCESS_TOKEN_PRIVATE_KEY, + { expiresIn: '1d' } + ); + + statusCode = 200; + + // return all information about the user + result = { + status: true, + message: 'Access token created successfully', + data: { + accessToken, + }, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const getUserHandler = async (req, reply) => { + try { + let user = await User.findOne({ + where: { id: req.user.id }, + }); + + if (!user) + return reply.code(404).send({ + status: false, + message: 'User Not Found', + }); + + statusCode = 200; + + result = { + status: true, + message: 'User fetched successfully', + data: user, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +/** + * view all users + * @returns {object} - information about the user. + */ +const getUsersHandler = async (req, reply) => { + try { + let users = await User.findAll(); + + statusCode = 200; + + result = { + status: true, + message: 'Users retrieved successfully', + data: users, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +/** + * update user role + * @returns {object} - information about the user. + */ +const updateUserRoleHandler = async (req, reply) => { + try { + let { role, email } = req.body; + let isAdmin = req.user.role; + + if (!isAdmin) + return reply.code(401).send({ + status: false, + message: 'You are not authorized to perform this action', + }); + + let user = await User.findOne({ + where: { email }, + }); + + if (user.id === req.user.id) { + return reply.code(400).send({ + status: false, + message: 'You are not allowed to perform this action', + }); + } + + if (user.super) { + return reply.code(400).send({ + status: false, + message: 'You are not allowed to perform this action', + }); + } + + if (!user.status) + return reply.code(400).send({ + status: false, + message: 'User has not completed registration', + }); + + if (!user) + return reply.code(404).send({ + status: false, + message: 'User Not Found', + }); + + await user.update({ role }); + + statusCode = 200; + + result = { + status: true, + message: 'User role updated successfully', + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const updatePasswordHandler = async (req, reply) => { + try { + const { currentPassword, newPassword } = req.body; + const { id } = req.user; + + const user = await User.findOne({ where: { id } }); + + if (!user) + return reply.code(404).send({ + status: false, + message: 'User not found', + }); + + // check if the current password is correct + const isMatch = await bcrypt.compare(currentPassword, user.password); + + if (!isMatch) + return reply.code(400).send({ + status: false, + message: 'Your current password does not match the old password', + }); + + // hash the new password + const encryptedPassword = await bcrypt.hash(newPassword, 10); + + // update the password + await user.update({ password: encryptedPassword }); + + statusCode = 200; + + result = { + status: true, + message: 'Password updated successfully', + data: null, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const forgotPasswordHandler = async (req, reply) => { + try { + const { email } = req.body; + + // check if email exist and status is false + let userExists = await findUserByEmailStatus(email, true); + + if (!userExists) + return reply.code(409).send({ + status: false, + message: 'User Not Found', + }); + + // encrypt the email + let ciphertext = CryptoJS.AES.encrypt( + userExists.email, + 'ManifoldSecret' + ).toString(); + + // remove special characters to the token + let updatedCipherText = ciphertext + .toString() + .replaceAll('+', 'xMl3Jk') + .replaceAll('/', 'Por21Ld') + .replaceAll('=', 'Ml32'); + + const details = { + name: userExists.firstName, + templateToUse: 'passwordReset', + url: `${config.BASE_URL}/reset-password/${updatedCipherText}`, + }; + + // invite user by sending an email + await sendEmail( + email, + 'Reset Manifold Forecast Password', + 'Please click the link below to reset password', + details + ); + + statusCode = 200; + + result = { + status: true, + message: 'Password reset link sent successfully', + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const resetPasswordHandler = async (req, reply) => { + try { + const { password, token } = req.body; + + // add special characters to the token + let updatedToken = token + .toString() + .replaceAll('xMl3Jk', '+') + .replaceAll('Por21Ld', '/') + .replaceAll('Ml32', '='); + + // decrypt the invitation token + let bytes = CryptoJS.AES.decrypt(updatedToken, 'ManifoldSecret'); + let email = bytes.toString(CryptoJS.enc.Utf8); + + // check if email exist and status is false + let userExists = await findUserByEmailStatus(email, true, false); + + if (!userExists) + return reply.code(409).send({ + status: false, + message: 'User Not Found', + }); + + // hash the new password + const encryptedPassword = await bcrypt.hash(password, 10); + + // update the password + await userExists.update({ password: encryptedPassword }); + + statusCode = 200; + + result = { + status: true, + message: 'Password reset successfully', + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const deleteUserHandler = async (req, reply) => { + try { + // TODO:: only admin should be able to delete a user + const id = req.params.id; + + let user = await User.findOne({ + where: { id }, + }); + + if (!user) + return reply.code(404).send({ + status: false, + message: 'User Not Found', + }); + + if (user.id === req.user.id) { + return reply.code(400).send({ + status: false, + message: 'You are not allowed to perform this action', + }); + } + + if (user.super) { + return reply.code(400).send({ + status: false, + message: 'You are not allowed to perform this action', + }); + } + + user.destroy(); + + statusCode = 200; + + result = { + status: true, + message: 'User deleted successfully', + data: {}, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +module.exports = { + inviteUserHandler, + loginUserHandler, + registerUserHandler, + getUserHandler, + refreshTokenHandler, + updatePasswordHandler, + forgotPasswordHandler, + resetPasswordHandler, + getUsersHandler, + updateUserRoleHandler, + deleteUserHandler, + savePrepaidPaymentHandler, + saveAccruedPaymentHandler, + updateAccruedPaymentHandler, + updatePrepaidPaymentHandler, +}; diff --git a/src/controllers/handlers/exchange.handler.js b/src/controllers/handlers/exchange.handler.js new file mode 100644 index 0000000..c0817f1 --- /dev/null +++ b/src/controllers/handlers/exchange.handler.js @@ -0,0 +1,615 @@ +const { default: axios } = require("axios"); +const ExcelJS = require("exceljs"); +const db = require("../../models"); +const config = require("../../../config"); +const { Op } = require("sequelize"); +const Rate = db.rates; +const BillForecast = db.billForecasts; +const InvoiceForecast = db.invoiceForecasts; +const SaleForecast = db.saleForecasts; +const PurchaseForecast = db.purchaseForecasts; +const Invoice = db.invoices; +const ZohoRate = db.zohorates; +const Bill = db.bills; +const InitialBalance = db.initialBalances; +const Sale = db.sales; +const Purchase = db.purchases; +const CustomerPayment = db.customerPayments; +const VendorPayment = db.vendorPayments; + + +//get inventory +const getZohoInventoryHandler = async(req,reply) =>{ + try{ + const zohoAccessToken = req.body.accessToken; + if(zohoAccessToken === null || zohoAccessToken===""){ + + return reply.code(400).send({ + status: false, + message: "missing access token", + }); + } + const options = { + headers: { + "Content-Type": ["application/json"], + Authorization: "Bearer " + zohoAccessToken, + }, + }; + console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>""""""""""""""""""""'); + + var url =`${config.ZOHO_BOOK_BASE_URL}/items?organization_id=${config.ORGANIZATION_ID}`; + //https://books.zoho.com/api/v3/items?organization_id={{Organization.Organization_ID}} + console.log(url); + + res = await axios.get(url, options); + + if(res.data.error) + { + return reply.code(400).send('error occurred'); + } + else if(res.data.code == 0 || res.data.message == 'success'){ +// Filter items with available_stock greater than 0 +const filteredItems = res.data.items.filter(item => item.available_stock > 0); + +// Map filtered items to include only item_id, name, and available_stock +console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + +const transformedItems = filteredItems.map(item => ({ + item_id: item.item_id, + name: item.name, + available_stock: item.available_stock, + account_name:item.account_name, + description:item.purchase_description, + rate:item.rate, + totalRate: item.rate*item.available_stock, + manufacturer:item.manufacturer +})); +var totalsumofItems = transformedItems.reduce((acc,currval)=>acc +currval.totalRate,0) +var resultTobereturn = { + status: true, + TotalsumofItems:totalsumofItems, + + message: "Items fetched successfully", + data: {Items:transformedItems,totalsumofItems:totalsumofItems} +} +console.clear(); +console.log(resultTobereturn) +return reply.code(200).send(resultTobereturn); + + } + else { + return reply.code(400).send({ + status: false, + message: "Failed to fetch items", + }); +} + +} +catch(e){ + statusCode = e.code; + result = { + status: false, + message: e.message, + }; +} +} +//get the non billable expenses +/*const getNonbillableExpense = async(req,reply)=> +{ + console.clear() + console.log("i am insite the word") + try + { + const zohoAccessToken = req.body.accessToken; + console.log(zohoAccessToken,"rttgghxvj zbxz") + if(zohoAccessToken == null || zohoAccessToken=="") + { + + return reply.code(400).send({ + status: false, + message: "missing access token", + }); + } + const options = { + headers: { + "Content-Type": ["application/json"], + Authorization: "Bearer " + zohoAccessToken, + }, + }; + var url = `${config.ZOHO_BOOK_BASE_URL}/expenses?organization_id=${config.ORGANIZATION_ID}`; + console.log(url,"this is the url") + var res = await axios.get(url, options); + console.log(res,"this is the response"); + console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + if(res.data.error){ + return reply.code(400).send('error occurred'); + } + else if(res.data.code == 0 || res.data.message == 'success'){ + const filteredItems = res.data.expenses.filter(item => item.is_billable == false); + const transformedItems = filteredItems.map(item => ({ + expense_id: item.expense_id, + date: item.date, + account_name: item.account_name, + description: item.description, + paid_through_account_name: item.paid_through_account_name, + currency_code: item.currency_code, + currency_id: item.currency_id, + total_without_tax: item.total_without_tax, + status: item.status, + + })); + + return reply.code(200).send({ + status: true, + message: "Items fetched successfully", + data: transformedItems, + }); + + } + else { + return reply.code(400).send({ + status: false, + message: "Failed to fetch items", + }); +} + + +} + catch(e){ + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } +}*/ + +const getNonbillableExpense = async (req, reply) => { + try { + console.clear(); + + const hzohoAccessToken = req.body.accessToken; + let getZohoTokenurl = `${config.ZOHO_BASE_URL}?refresh_token=${config.ZOHO_REFRESH_TOKEN}&client_id=${config.ZOHO_CLIENT_ID}&client_secret=${config.ZOHO_CLIENT_SECRET}&redirect_uri=${config.BASE_URL}&grant_type=refresh_token`; + console.log(getZohoTokenurl, "access token"); // Corrected variable name + try { + let zohoRes = await axios.post(getZohoTokenurl); + // Check if access token exists in the response data + var zohoToken = zohoRes.data.access_token;//zohoRes.data.access_token; + + console.log(zohoToken) + if (!zohoToken || zohoToken == null) { + return reply.code(400).send({ + status: false, + message: "Missing access token", + }); + } + } catch (error) { + console.error("Error fetching Zoho token:"); + return reply.code(500).send({ + status: false, + message: "Failed to fetch Zoho token", + }); + } + const options = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${zohoToken}`, + }, + }; + + const url = `${config.ZOHO_BOOK_BASE_URL}/expenses?organization_id=${config.ORGANIZATION_ID}`; + + const response = await axios.get(url, options); + + if (response.data.error) { + return reply.code(400).send('Error occurred'); + } else if (response.data.code === 0 || response.data.message === 'success') { + const filteredItems = response.data.expenses.filter(item => !item.is_billable); + const transformedItems = filteredItems.map(item => ({ + expense_id: item.expense_id, + date: item.date, + account_name: item.account_name, + description: item.description, + paid_through_account_name: item.paid_through_account_name, + currency_code: item.currency_code, + currency_id: item.currency_id, + total_without_tax: item.total_without_tax, + status: item.status, + })); + + return reply.code(200).send({ + status: true, + message: "Items fetched successfully", + data: transformedItems, + }); + } else { + return reply.code(400).send({ + status: false, + message: "Failed to fetch items", + }); + } + } catch (error) { + console.error('Error:', error); + return reply.code(500).send({ + status: false, + message: "Internal Server Error", + }); + } +}; + +const loginUserHandler = async (req, reply) => { + const { email, password } = req.body; + const status = true; + + try { + // check if email exists + let user = await findUserByEmailStatus(email, status); + + if (!user) + return reply.code(500).send({ + status: false, + message: 'Invalid email or password', + }); + + // check if password matches + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) + return reply.code(401).send({ + status: false, + message: 'Invalid email or password', + }); + + // generate tokens + const { accessToken, refreshToken } = await generateTokens(user); + + statusCode = 200; + + result = { + status: true, + message: 'User logged in successfully', + data: { + accessToken, + refreshToken, + isZohoAuthenticated: user.isZohoAuthenticated ? true : false, + }, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; +// Get exchange rate and save into database +const getZohoExchangeRateHandler = async ( + zohoAccessToken, + forecastNumber, + forecastPeriod, + userId +) => { + const TODAY_START = new Date().setHours(0, 0, 0, 0); + const TODAY_END = new Date().setHours(23, 59, 59, 999); + let rate; + + const options = { + headers: { + "Content-Type": ["application/json"], + Authorization: "Bearer " + zohoAccessToken, + }, + }; + + // check if exchange rate exist in db for the day + + rate = await Rate.findOne({ + where: { + userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + // if rate doesn't exist then create the exchange rate from zoho and save to db + if (!rate) { + url = `${config.ZOHO_BOOK_BASE_URL}/settings/currencies/${config.DOLLAR_CURRENCY_ID}/exchangerates?organization_id=${config.ORGANIZATION_ID}`; + + res = await axios.get(url, options); + + if (res.data.error) + return reply.code(400).send({ + status: false, + message: "Could not fetch exchange rate", + }); + + // save to db + rate = await Rate.create({ + userId, + old: res.data.exchange_rates[0].rate, + latest: res.data.exchange_rates[0].rate, + forecastType: `${forecastNumber} ${forecastPeriod}`, + }); + } + + return rate; +}; + +const getExchangeRateHandler = async (req, reply) => { + try { + const { number, period } = req.params; + const TODAY_START = new Date().setHours(0, 0, 0, 0); + const TODAY_END = new Date().setHours(23, 59, 59, 999); + const userId = req.user.id; + + let rate = await Rate.findOne({ + where: { + userId, + forecastType: number + " " + period, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + if (!rate) { + return reply.code(400).send({ + status: false, + message: "Could not fetch exchange rate", + }); + } + + statusCode = 200; + + result = { + status: true, + message: "Exchange rate fetched successfully", + data: rate, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const getAllExchangeRateHandler = async (req, reply) => { + try { + const zohorates = await ZohoRate.findAll({ + limit: 30, + order: [["createdAt", "DESC"]], + }); + + statusCode = 200; + + result = { + status: true, + message: "Exchage rates fetched successfully", + data: zohorates, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const downloadExchangeRate = async (req, reply) => { + try { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Exchange Rate List"); + + const exchangeRateColumns = [ + { key: "id", header: "id" }, + { key: "rate", header: "rate" }, + { key: "createdAt", header: "date" }, + ]; + + worksheet.columns = exchangeRateColumns; + + const zohorates = await ZohoRate.findAll({ + order: [["createdAt", "DESC"]], + }); + + zohorates.forEach((rate) => { + worksheet.addRow(rate); + }); + + result = await workbook.xlsx.writeBuffer(); + statusCode = 200; + } catch (e) { + statusCode = e.response.status; + result = { + status: false, + message: e.response.data.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const updateExchangeRateHandler = async (req, reply) => { + try { + const TODAY_START = new Date().setHours(0, 0, 0, 0); + const TODAY_END = new Date().setHours(23, 59, 59, 999); + const { id } = req.params; + const { latest, forecastNumber, forecastPeriod } = req.body; + const userId = req.user.id; + + // check if exchange rate exist in db for the day + let rate = await Rate.findOne({ + where: { + id, + userId, + }, + }); + + // if rate doesn't exist then create the exchange rate from zoho and save to db + if (!rate) { + return reply.code(400).send({ + status: false, + message: "Could not fetch exchange rate", + }); + } + + rate = await rate.update({ + userId: userId, + latest, + }); + + await InitialBalance.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await BillForecast.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await InvoiceForecast.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await SaleForecast.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await PurchaseForecast.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Bill.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Invoice.destroy({ + where: { + userId: userId, + forecastType: `${forecastNumber} ${forecastPeriod}`, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Sale.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Purchase.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await CustomerPayment.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await VendorPayment.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + statusCode = 200; + + result = { + status: true, + message: "Exchange rate updated successfully", + data: rate, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +module.exports = { + getExchangeRateHandler, + getAllExchangeRateHandler, + getZohoExchangeRateHandler, + updateExchangeRateHandler, + downloadExchangeRate, + getNonbillableExpense, + getZohoInventoryHandler +}; diff --git a/src/controllers/handlers/forecast.handler.js b/src/controllers/handlers/forecast.handler.js new file mode 100644 index 0000000..e617030 --- /dev/null +++ b/src/controllers/handlers/forecast.handler.js @@ -0,0 +1,999 @@ +const { default: axios } = require("axios"); +const ExcelJS = require("exceljs"); +const db = require("../../models"); +const { Op } = require("sequelize"); +let moment = require("moment"); +const config = require("../../../config"); +const { + createZohoRate, + createOpeningBalance, + createBankAccounts, + getTodayDayOpeningBalance, + fetchAllInvoiceForecast, + fetchAllBillForecast, + fetchAllPurchaseForecast, + fetchAllSaleForecast, +} = require("../../helpers/dbQuery"); + +const InvoiceForecast = db.invoiceForecasts; +const SaleForecast = db.saleForecasts; +const PurchaseForecast = db.purchaseForecasts; +const BillForecast = db.billForecasts; +const Invoice = db.invoices; +const Sale = db.sales; +const AccruedExpDb = db.accruedexp; +const getPrepaidExpDB = db.prepaidexp; +const Purchase = db.purchases; +const CustomerPayment = db.customerPayments; +const VendorPayment = db.vendorPayments; +const Bill = db.bills; +const InitialBalance = db.initialBalances; +const BankAccount = db.bankAccounts; +const Rate = db.rates; +const Overdraft = db.overdrafts; +const OpeningBalance = db.openingBalances; + +const resyncHandler = async (req, reply) => { + const TODAY_START = moment().startOf("day").format(); + const TODAY_END = moment().endOf("day").format(); + try { + const userId = req.user.id; + + await InitialBalance.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await InvoiceForecast.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await SaleForecast.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await PurchaseForecast.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await BillForecast.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Invoice.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Sale.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Purchase.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Bill.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await CustomerPayment.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await VendorPayment.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + await Rate.destroy({ + where: { + userId: userId, + updatedAt: { + [Op.gt]: TODAY_START, + [Op.lt]: TODAY_END, + }, + }, + }); + + statusCode = 204; + + result = { + status: true, + message: "Resynced successfully", + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; +const ACcruedExpHandler = async (req, reply) => { + let statusCode = 200; + let result; + + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); // Set hours, minutes, seconds, and milliseconds to 0 for comparison with dates only + + const getAllAccrued = await AccruedExpDb.findAll(); + + const filteredExpenses = getAllAccrued.filter(expense => { + const paymentDate = new Date(expense.payment_date); + + return expense.paymentConfirmation === 0 && paymentDate < today; + }); + + + result = { + status: true, + message: "Accrued fetched successfully", + data: filteredExpenses, + }; + } catch (e) { + statusCode = e.code || 500; // Set status code to 500 if e.code is undefined + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); + +}; +const PrepaidExpHandler = async (req, reply) => { + let statusCode = 200; + console.log('this place',' >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); + + try{ + const today = new Date(); + let getAllprepaid = await getPrepaidExpDB.findAll(); + + console.log(getAllprepaid,' >>>>>>>>>>>>>>>>>>>>hhhhhhhhhhhhhhhhh>'); + + const filteredExpenses = getAllprepaid.filter(expense => { + const paymentDate = new Date(expense.payment_date); + + return expense.paymentConfirmation === 0 && paymentDate < today; + }); + + var result = { + status: true, + message: "Prepaid Expenses fetched successfully", + data: filteredExpenses, + }; + + } + catch (e) { + statusCode = 500; + result = { + status: false, + message: e.message, + }; +} +return reply.status(statusCode).send(result); + +}; +const bankAccountsHandler = async (req, reply) => { + const YESTERDAY_START = moment().subtract(1, "days").startOf("day").format(); + const YESTERDAY_END = moment().subtract(1, "days").endOf("day").format(); + try { + let bankAccounts = await BankAccount.findAll({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + statusCode = 200; + + result = { + status: true, + message: "Opening bank accounts fetched successfully", + data: bankAccounts, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const downloadOpeningBalance = async (req, reply) => { + try { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Opening Balance"); + const YESTERDAY_START = moment() + .subtract(1, "days") + .startOf("day") + .format(); + const YESTERDAY_END = moment().subtract(1, "days").endOf("day").format(); + const openingBlanceColumns = [ + { key: "id", header: "id" }, + { key: "accountName", header: "account name" }, + { key: "accoutType", header: "account type" }, + { key: "currency", header: "currency" }, + { key: "balance", header: "balance" }, + ]; + + worksheet.columns = openingBlanceColumns; + + let bankAccounts = await BankAccount.findAll({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + bankAccounts.forEach((rate) => { + worksheet.addRow(rate); + }); + + result = await workbook.xlsx.writeBuffer(); + statusCode = 200; + } catch (e) { + statusCode = e.response.status; + result = { + status: false, + message: e.response.data.message, + }; + } + return reply.status(statusCode).send(result); +}; + +// This function will be executed by a CRON JOB daily +// stores todays exchange rate and opening in the database +const createOpeningBalanceHandler = async (req, reply) => { + const TODAY_START = moment().startOf("day").format(); + const TODAY_END = moment().endOf("day").format(); + try { + let todayOpeningBalData = { + today_start: TODAY_START, + today_end: TODAY_END, + }; + // check if opening balance has been updated today. + const openingBalance = await getTodayDayOpeningBalance({ + todayOpeningBalData, + }); + + if (openingBalance) { + return reply.code(400).send({ + status: false, + message: "Opening Balance Already Exits for today", + }); + } + + // get zoho access token + url = `${config.ZOHO_BASE_URL}?refresh_token=${config.ZOHO_REFRESH_TOKEN}&client_id=${config.ZOHO_CLIENT_ID}&client_secret=${config.ZOHO_CLIENT_SECRET}&redirect_uri=${config.ZOHO_REDIRECT_URI}&grant_type=refresh_token`; + + zoho = await axios.post(url); + + if (zoho.data.error) { + return reply.code(400).send({ + status: false, + message: "Invalid code", + }); + } + + const options = { + headers: { + "Content-Type": ["application/json"], + Authorization: "Bearer " + zoho.data.access_token, + }, + }; + + // get current exchange rate from zoho + let rateUrl = `${config.ZOHO_BOOK_BASE_URL}/settings/currencies/${config.DOLLAR_CURRENCY_ID}/exchangerates?organization_id=${config.ORGANIZATION_ID}`; + + res = await axios.get(rateUrl, options); + + if (res.data.error) + return reply.code(400).send({ + status: false, + message: "Could not fetch exchange rate", + }); + + payload = { + rate: res.data.exchange_rates[0].rate, + }; + + await createZohoRate({ payload }); + + // fetch all bank accounts details + let bankAccountUrl = `${config.ZOHO_BOOK_BASE_URL}/bankaccounts?organization_id=${config.ORGANIZATION_ID}`; + + resp = await axios.get(bankAccountUrl, options); + + if (resp.data.error) + return reply.code(400).send({ + status: false, + message: "Could not fetch bank accounts", + }); + + let ngnBalance = 0; + let usdBalance = 0; + let overdraft; + + for (const [rowNum, inputData] of resp.data.bankaccounts.entries()) { + // Save into databse + if ( + inputData.currency_code === "USD" || + inputData.currency_code === "NGN" + ) { + overdraft = inputData.balance; + // if bank balance is less than zero then check if there is an overdraft account loan + if (inputData.balance < 0) { + overdraftExits = await Overdraft.findOne({ + where: { accountId: inputData.account_id }, + }); + + if (overdraftExits) { + overdraft = + parseFloat(inputData.balance) + + parseFloat(overdraftExits.dataValues.amount); + } + } + + let bankAccounts = { + accountId: inputData.account_id, + accountName: inputData.account_name, + accountType: inputData.account_type, + accountNumber: inputData.account_number, + bankName: inputData.bank_name, + currency: inputData.currency_code, + balance: inputData.balance, + overdraftBalance: overdraft, + }; + + if (inputData.currency_code === "USD") { + usdBalance += overdraft; + } + + if (inputData.currency_code === "NGN") { + ngnBalance += overdraft; + } + + await createBankAccounts({ bankAccounts }); + } + } + + payload = { + naira: ngnBalance, + dollar: usdBalance, + }; + + await createOpeningBalance({ payload }); + + statusCode = 200; + + result = { + status: true, + message: "Successfully", + data: "", + }; + } catch (e) { + result = { + status: false, + message: e.response.data.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const createOverdraftHandler = async (req, reply) => { + const YESTERDAY_START = moment().subtract(1, "days").startOf("day").format(); + const YESTERDAY_END = moment().subtract(1, "days").endOf("day").format(); + const { + accountId, + accountName, + accountType, + accountNumber, + bankName, + currency, + amount, + } = req.body; + try { + let overdraft; + let bankAccount; + let openingBalance; + let bankBalance; + + let overdraftExists = await Overdraft.findOne({ + where: { accountId }, + }); + + if (overdraftExists) + return reply.code(409).send({ + status: false, + message: "Overdraft account already exist", + }); + + bankAccount = await BankAccount.findOne({ + where: { + accountId, + currency, + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + if (!bankAccount) + return reply.code(404).send({ + status: false, + message: "Bank account not found", + }); + + overdraft = await Overdraft.create({ + accountId, + accountName, + accountType, + accountNumber, + bankName, + currency, + amount, + }); + + if (!overdraft) + return reply.code(500).send({ + status: false, + message: "Unable to create overdraft account", + }); + + if (bankAccount.currency !== currency) + return reply.code(400).send({ + status: false, + message: "Curreny mismtach", + }); + + bankBalance = parseFloat(bankAccount.dataValues.balance); + let overdraftBalance = parseFloat(amount) + parseFloat(bankBalance); + + // update opening balance + openingBalance = await OpeningBalance.findOne({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + //update bankAccount + bankAccount = await bankAccount.update({ + overdraftBalance: overdraftBalance, + }); + + if (!bankAccount) + return reply.code(500).send({ + status: false, + message: "Unable to update bank account", + }); + + let res = await BankAccount.findAll({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + let ngnBalance = 0; + let usdBalance = 0; + + res.map(async function (res) { + if (res.dataValues.currency === "USD") { + usdBalance += parseFloat(res.dataValues.overdraftBalance); + } + if (res.dataValues.currency === "NGN") { + ngnBalance += parseFloat(res.dataValues.overdraftBalance); + } + }); + + openingBalance = await openingBalance.update({ + dollar: usdBalance, + naira: ngnBalance, + }); + + if (!openingBalance) + return reply.code(500).send({ + status: false, + message: "Unable to update opening balance", + }); + + statusCode = 201; + + result = { + status: true, + message: "Overdraft account created successfully", + }; + } catch (e) { + statusCode = e.response.status; + result = { + status: false, + message: e.response.data.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const updateOverdraftHandler = async (req, reply) => { + const YESTERDAY_START = moment().subtract(1, "days").startOf("day").format(); + const YESTERDAY_END = moment().subtract(1, "days").endOf("day").format(); + const id = req.params.id; + const { amount } = req.body; + + try { + let openingBalance; + + let overdraft = await Overdraft.findOne({ + where: { accountId: id }, + }); + + if (!overdraft) + return reply.code(500).send({ + status: false, + message: "Overdraft account not found", + }); + + let bankAccount = await BankAccount.findOne({ + where: { + accountId: overdraft.accountId, + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + if (!bankAccount) + return reply.code(404).send({ + status: false, + message: "Bank account not found", + }); + + // update opening balance + openingBalance = await OpeningBalance.findOne({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + if (!openingBalance) + return reply.code(404).send({ + status: false, + message: "Opening Balance not found", + }); + + let overdraftDiff; + + if (parseFloat(amount) > parseFloat(overdraft.dataValues.amount)) { + overdraftDiff = + parseFloat(amount) - parseFloat(overdraft.dataValues.amount); + + overdraftBalance = + parseFloat(bankAccount.dataValues.overdraftBalance) + + parseFloat(overdraftDiff); + + if (bankAccount.dataValues.currency === "NGN") { + dollar = openingBalance.dataValues.dollar; + naira = + parseFloat(openingBalance.dataValues.naira) + + parseFloat(overdraftDiff); + } + + if (bankAccount.dataValues.currency === "USD") { + naira = openingBalance.dataValues.naira; + dollar = + parseFloat(openingBalance.dataValues.dollar) + + parseFloat(overdraftDiff); + } + + bankAccount = await bankAccount.update({ + overdraftBalance: overdraftBalance, + }); + + openingBalance = await openingBalance.update({ + dollar, + naira, + }); + } else { + overdraftDiff = + parseFloat(amount) - parseFloat(overdraft.dataValues.amount); + + overdraftBalance = + parseFloat(bankAccount.dataValues.overdraftBalance) + + parseFloat(overdraftDiff); + + bankAccount = await bankAccount.update({ + overdraftBalance: overdraftBalance, + }); + + if (!bankAccount) + return reply.code(500).send({ + status: false, + message: "Unable to update bank account", + }); + + let res = await BankAccount.findAll({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + let ngnBalance = 0; + let usdBalance = 0; + + res.map(async function (res) { + if (res.dataValues.currency === "USD") { + usdBalance += parseFloat(res.dataValues.overdraftBalance); + } + if (res.dataValues.currency === "NGN") { + ngnBalance += parseFloat(res.dataValues.overdraftBalance); + } + }); + + openingBalance = await openingBalance.update({ + dollar: usdBalance, + naira: ngnBalance, + }); + + if (!openingBalance) + return reply.code(500).send({ + status: false, + message: "Unable to update opening balance", + }); + } + + overdraft = await overdraft.update({ + amount: amount, + }); + + if (!overdraft) + return reply.code(500).send({ + status: false, + message: "Unable to update overdraft", + }); + + statusCode = 200; + + result = { + status: true, + message: "Overdraft account updated successfully", + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const deleteOverdraftHandler = async (req, reply) => { + const YESTERDAY_START = moment().subtract(1, "days").startOf("day").format(); + const YESTERDAY_END = moment().subtract(1, "days").endOf("day").format(); + const id = req.params.id; + + try { + let overdraft = await Overdraft.findOne({ + where: { accountId: id }, + }); + + if (!overdraft) + return reply.code(500).send({ + status: false, + message: "Overdraft account not found", + }); + + let bankAccount = await BankAccount.findOne({ + where: { + accountId: overdraft.accountId, + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + if (!bankAccount) + return reply.code(404).send({ + status: false, + message: "Bank account not found", + }); + + let openingBalance = await OpeningBalance.findOne({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + if (!openingBalance) + return reply.code(404).send({ + status: false, + message: "Opening Balance not found", + }); + + await overdraft.destroy(); + + bankAccount = await bankAccount.update({ + overdraftBalance: bankAccount.dataValues.balance, + }); + + if (!bankAccount) + return reply.code(500).send({ + status: false, + message: "Unable to update bank account", + }); + + let res = await BankAccount.findAll({ + where: { + createdAt: { + [Op.gt]: YESTERDAY_START, + [Op.lt]: YESTERDAY_END, + }, + }, + }); + + let ngnBalance = 0; + let usdBalance = 0; + + res.map(async function (res) { + if (res.dataValues.currency === "USD") { + usdBalance += parseFloat(res.dataValues.overdraftBalance); + } + if (res.dataValues.currency === "NGN") { + ngnBalance += parseFloat(res.dataValues.overdraftBalance); + } + }); + + openingBalance = await openingBalance.update({ + dollar: usdBalance, + naira: ngnBalance, + }); + + if (!openingBalance) + return reply.code(500).send({ + status: false, + message: "Unable to update opening balance", + }); + + statusCode = 200; + + result = { + status: true, + message: "Overdraft deleted successfully", + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const getOverdraftHandler = async (req, reply) => { + try { + let overdrafts = await Overdraft.findAll(); + + statusCode = 200; + + result = { + status: true, + message: "Overdrafts retrieved successfully", + data: overdrafts, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +const getChartDataHandler = async (req, reply) => { + try { + const { forecastNumber, forecastPeriod } = req.body; + const userId = req.user.id; + + const TODAY_START = moment().startOf("day").format(); + const TODAY_END = moment().endOf("day").format(); + + let payload = { + userId: userId, + forecastNumber: forecastNumber, + forecastPeriod: forecastPeriod, + today_start: TODAY_START, + today_end: TODAY_END, + }; + + const forecastMonths = []; + const forecastNairaChartInflow = []; + const forecastNairaChartOutflow = []; + const forecastDollarChartInflow = []; + const forecastDollarChartOutflow = []; + + let purchaseForecasts = await fetchAllPurchaseForecast({ payload }); + + let saleForecasts = await fetchAllSaleForecast({ payload }); + + // get invoice forecast where forecastType + let invoiceForecasts = await fetchAllInvoiceForecast({ payload }); + + let billForecasts = await fetchAllBillForecast({ payload }); + + for (let i = 0; i < forecastNumber; i++) { + const month = moment().add(i, "months").format("MMMM"); + forecastMonths.push(month); + } + + for (i = 0; i < invoiceForecasts.rows.length; i++) { + //INFLOW TOTAL + let invoiceForeacastClosingBalance = + invoiceForecasts.rows[i].currency === "NGN" + ? invoiceForecasts.rows[i].nairaClosingBalance + : invoiceForecasts.rows[i].dollarClosingBalance; + let saleForecastClosingBalance = + saleForecasts.rows[i].currency === "NGN" + ? saleForecasts.rows[i].nairaClosingBalance + : saleForecasts.rows[i].dollarClosingBalance; + + // OUTFLOW TOTAL + let billForeacastClosingBalance = + billForecasts.rows[i].currency === "NGN" + ? billForecasts.rows[i].nairaClosingBalance + : billForecasts.rows[i].dollarClosingBalance; + let purchaseForecastClosingBalance = + purchaseForecasts.rows[i].currency === "NGN" + ? purchaseForecasts.rows[i].nairaClosingBalance + : purchaseForecasts.rows[i].dollarClosingBalance; + + if (invoiceForecasts.rows[i].currency === "NGN") { + forecastNairaChartInflow.push( + (parseFloat(invoiceForeacastClosingBalance) + + parseFloat(saleForecastClosingBalance)).toFixed(2) + ); + forecastNairaChartOutflow.push( + (parseFloat(billForeacastClosingBalance) + + parseFloat(purchaseForecastClosingBalance)).toFixed(2) + ); + + } else { + + forecastDollarChartInflow.push( + (parseFloat(invoiceForeacastClosingBalance) + + parseFloat(saleForecastClosingBalance)).toFixed(2) + ); + forecastDollarChartOutflow.push( + (parseFloat(billForeacastClosingBalance) + + parseFloat(purchaseForecastClosingBalance)).toFixed(2) + ); + } + } + + statusCode = 200; + result = { + status: true, + data: { + "months": forecastMonths, + "forecastNairaInflow": forecastNairaChartInflow, + "forecastNairaOutflow": forecastNairaChartOutflow, + "forecastDollarInflow": forecastDollarChartInflow, + "forecastDollarOutflow": forecastDollarChartOutflow, + }, + }; + } catch (e) { + statusCode = e.response.status; + result = { + status: false, + message: e.message, + }; + } + return reply.status(statusCode).send(result); +}; + +module.exports = { + resyncHandler, + bankAccountsHandler, + createOpeningBalanceHandler, + createOverdraftHandler, + updateOverdraftHandler, + deleteOverdraftHandler, + getOverdraftHandler, + getChartDataHandler, + downloadOpeningBalance, + PrepaidExpHandler, + ACcruedExpHandler, +}; diff --git a/src/controllers/handlers/inflow.handler.js b/src/controllers/handlers/inflow.handler.js new file mode 100644 index 0000000..49f0599 --- /dev/null +++ b/src/controllers/handlers/inflow.handler.js @@ -0,0 +1,7 @@ +//getInvoice + +// getCustomerPayments + +// getSalesOrder + +//processSales \ No newline at end of file diff --git a/src/controllers/handlers/outflow.handler.js b/src/controllers/handlers/outflow.handler.js new file mode 100644 index 0000000..e26502b --- /dev/null +++ b/src/controllers/handlers/outflow.handler.js @@ -0,0 +1,7 @@ +// getBill + +// getVendorPayments + +// getPurchaseOrder + +// processPurchases \ No newline at end of file diff --git a/src/controllers/handlers/token.handler.js b/src/controllers/handlers/token.handler.js new file mode 100644 index 0000000..5f09db8 --- /dev/null +++ b/src/controllers/handlers/token.handler.js @@ -0,0 +1,190 @@ +const { default: axios } = require('axios'); +const db = require('../../models'); +const User = db.users; +const Token = db.tokens; +const dayjs = require('dayjs'); +var utc = require('dayjs/plugin/utc'); +var timezone = require('dayjs/plugin/timezone'); +const config = require('../../../config'); + +dayjs.extend(utc); +dayjs.extend(timezone); + +// dayjs.tz.setDefault("Africa/Lagos") + +Date.prototype.addHours = function (h) { + this.setHours(this.getHours() + h); + return this; +}; + +const options = { + headers: { 'Content-Type': ['application/json'] }, +}; +// this generates zoho access token +const generateZohoTokenHandler = async (req, reply) => { + const { code } = req.body; +//console.log(code,">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + try { + let user; + let resp; + let url; + + // get current user + user = await User.findOne({ + where: { email: req.user.email }, + }); +//console.log(user,2222222222222222222222222222222222222222222222222222222222222) + if (!user) { + return reply.code(404).send({ + status: false, + message: 'User Not Found', + }); + } + + // check if user has zoho token + let zohoToken = await Token.findOne({ + where: { + userId: user.id, + }, + }); + console.log(zohoToken,2222222222222222222222222222222222222222222222222222222222222) + + + // new to zoho + if (!zohoToken) { + if (!code) { + return reply.code(400).send({ + status: false, + message: 'Code is required', + }); + } + + url = `${config.ZOHO_BASE_URL}?code=${code}&client_id=${config.ZOHO_CLIENT_ID}&client_secret=${config.ZOHO_CLIENT_SECRET}&redirect_uri=${config.BASE_URL}&grant_type=authorization_code`; + console.log(url,44444444444444444444444444444444444444444444444444444444444444444444444) + resp = await axios.post(url, options); + console.log(resp,44444444444444444444444444444444444444444444444444444444444444444444444) + + if (resp.data.error) + return reply.code(400).send({ + status: false, + message: 'Invalid code', + }); + + // store the zoho token in the zoho table + zohoToken = await Token.create({ + userId: user.id, + //zohoAccessToken: resp.data.access_token, + zohoRefreshToken: resp.data.refresh_token, + zohoTokenExpiry: dayjs().add(3600, 'second').utc(1).format(), + zohoTokenDate: new Date().addHours(1), + }); + } else { + url = `${config.ZOHO_BASE_URL}?refresh_token=1000.b19eafa36c01c36f065d567e6dead718.5f0ee96b622ad2a7d1a1dcd7169f5f31&client_id=${config.ZOHO_CLIENT_ID}&client_secret=${config.ZOHO_CLIENT_SECRET}&redirect_uri=${config.ZOHO_REDIRECT_URI}&grant_type=refresh_token`; +console.log(url, 'refresh tokeeennnnnnnnn22222222222222222222222222222222') + resp = await axios.post(url, options); + + if (resp.data.error) { + return reply.code(400).send({ + status: false, + message: 'Invalid code', + }); + } + + zohoToken = await zohoToken.update({ + // zohoAccessToken: resp.data.access_token, + zohoTokenExpiry: dayjs().add(3600, 'second').utc(1).format(), + zohoTokenDate: new Date().addHours(1), + }); + console.log(zohoToken, 'zohoToken tokeeennnnnnnnn22222222222222222222222222222222') + + } + user.update({ + isZohoAuthenticated: true, + }); + + statusCode = 200; + result = { + status: true, + message: 'Zoho Token generated successfully', + data: { + zohoAccessToken: resp.data.access_token, + zohoTokenExpiry: zohoToken.zohoTokenExpiry, + }, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const refreshZohoTokenHandler = async (req, reply) => { + try { + // get current user + let user = await User.findOne({ + where: { email: req.user.email }, + }); + + if (!user) { + return reply.code(404).send({ + status: false, + message: 'User Not Found', + }); + } + + // check if user has zoho token + let zohoToken = await Token.findOne({ + where: { + userId: user.id, + }, + }); + + if (!zohoToken) { + return reply.code(401).send({ + status: false, + message: 'Unauthorized access to zoho', + }); + } + + url = `${config.ZOHO_BASE_URL}?refresh_token=${zohoToken.zohoRefreshToken}&client_id=${config.ZOHO_CLIENT_ID}&client_secret=${config.ZOHO_CLIENT_SECRET}&redirect_uri=${config.ZOHO_REDIRECT_URI}&grant_type=refresh_token`; + + resp = await axios.post(url, options); + + if (resp.data.error) { + return reply.code(400).send({ + status: false, + message: 'Invalid code', + }); + } + + zohoToken = await zohoToken.update({ + // zohoAccessToken: resp.data.access_token, + zohoTokenExpiry: dayjs().add(3600, 'second').utc(1).format(), + zohoTokenDate: new Date().addHours(1), + }); + + statusCode = 200; + result = { + status: true, + message: 'Zoho Token generated successfully', + data: { + zohoAccessToken: resp.data.access_token, + zohoTokenExpiry: zohoToken.zohoTokenExpiry, + }, + }; + } catch (e) { + statusCode = e.code; + result = { + status: false, + message: e.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +module.exports = { generateZohoTokenHandler, refreshZohoTokenHandler }; diff --git a/src/controllers/handlers/zoho.handler.js b/src/controllers/handlers/zoho.handler.js new file mode 100644 index 0000000..ab0fe95 --- /dev/null +++ b/src/controllers/handlers/zoho.handler.js @@ -0,0 +1,2292 @@ +const { default: axios } = require('axios'); +const ExcelJS = require('exceljs'); +const config = require('../../../config'); +let moment = require('moment'); + +const { getZohoExchangeRateHandler } = require('./exchange.handler'); +const { + createInvoiceForecast, + createInvoice, + createBill, + createBillForecast, + createInitialBalance, + createPurchase, + createPurchaseForecast, + createSale, + createSaleForecast, + createVendorPayment, + createCustomerPayment, + getInitialBalance, + fetchAllInvoiceForecast, + fetchAllBillForecast, + fetchAllPurchaseForecast, + fetchAllSaleForecast, + fetchAllInvoice, + fetchAllBill, + fetchAllPurchase, + fetchAllSale, + fetchAllVendorPayments, + fetchAllCustomerPayments, + getPurchaseForecast, + getSaleForecast, + getPreviousDayOpeningBalance, + getVendorPaymentByVendorId, + getCustomerPaymentByCustomerId, +} = require('../../helpers/dbQuery'); +const db = require('../../models'); +const VendorPayment = db.vendorPayments; +const CustomerPayment = db.customerPayments; +const Purchase = db.purchases; +const Sale = db.sales; +moment().format(); + +const getInvoice = async ( + options, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let date = moment(); + let startDate; + let endDate; + + // monthly / yearly forecast + //TODO:: rework to fetch data by date... + startDate = date + .clone() + .add(0, forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(forecastNumber - 1, forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + let filteredInvoices = []; + let i = 1; + + do { + let url = `${config.ZOHO_BOOK_BASE_URL}/invoices?organization_id=${config.ORGANIZATION_ID}&due_date_end=${endDate}&sort_column=due_date&page=${i}`; + + resp = await axios.get(url, options); + + if (Array.isArray(resp.data.invoices) && resp.data.invoices.length > 0) { + const filteredPreviousInvoices = resp.data.invoices.filter( + (item, index) => + item.due_date < startDate && + (item.currency_code == 'NGN' || item.currency_code == 'USD') && + (item.status == 'sent' || + item.status == 'overdue' || + item.status == 'partially_paid' || + item.status == 'unpaid') + ); + + const filteredCurrentInvoices = resp.data.invoices.filter( + (item, index) => + item.due_date >= startDate && + item.due_date <= endDate && + (item.currency_code == 'NGN' || item.currency_code == 'USD') && + (item.status == 'sent' || + item.status == 'overdue' || + item.status == 'partially_paid' || + item.status == 'unpaid') + ); + + filteredPreviousInvoices.reduce(async function (a, e) { + e.due_date = startDate; + filteredInvoices.push(e); + return e; + }, 0); + + filteredCurrentInvoices.reduce(async function (a, e) { + filteredInvoices.push(e); + return e; + }, 0); + } + + ++i; + } while (!resp.data.invoices.length); + + for (i = 0; i < forecastNumber; i++) { + startDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + let newFilteredInvoices = filteredInvoices.filter( + (item, index) => + item.due_date >= startDate && + item.due_date <= endDate && + (item.status == 'sent' || + item.status == 'overdue' || + item.status == 'partially_paid' || + item.status == 'unpaid') + ); + + dollarClosingBalance = newFilteredInvoices.reduce(function (acc, obj) { + balance = obj.currency_code === 'USD' ? obj.balance : 0.0; + + return acc + balance; + }, 0); + + nairaClosingBalance = newFilteredInvoices.reduce(function (acc, obj) { + balance = + obj.currency_code === 'NGN' + ? obj.balance + : obj.balance * parseFloat(rate.latest).toFixed(2); + + if (rate.old !== rate.latest) { + balance = + (balance / parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + return acc + balance; + }, 0); + + await createInvoiceForecast( + userId, + parseFloat(nairaClosingBalance).toFixed(2), + 0.0, + startDate, + forecastNumber, + forecastPeriod, + 'NGN' + ); + + await createInvoiceForecast( + userId, + 0.0, + parseFloat(dollarClosingBalance).toFixed(2), + startDate, + forecastNumber, + forecastPeriod, + 'USD' + ); + + newFilteredInvoices.reduce(async function (a, e) { + if ( + parseFloat(e.balance) > 0 && + (e.due_date >= startDate || e.due_date <= endDate) + ) { + if ( + parseFloat(rate.old).toFixed(2) !== + parseFloat(rate.latest).toFixed(2) && + e.currency_code === 'NGN' + ) { + e.balance = + (parseFloat(e.balance).toFixed(2) / + parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + const payload = { + userId, + invoiceId: e.invoice_id, + customerName: e.customer_name, + status: e.status, + invoiceNumber: e.invoice_number, + refrenceNumber: e.reference_number, + date: e.date, + dueDate: e.due_date, + currencyCode: e.currency_code, + balance: e.balance, + naira: + e.currency_code === 'NGN' + ? e.balance + : e.balance * parseFloat(rate.latest).toFixed(2), + dollar: e.currency_code === 'USD' ? e.balance : 0.0, + exchangeRate: parseFloat(e.exchange_rate).toFixed(2), + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createInvoice({ payload }); + } + + return; + }, 0); + } +}; + +const getBill = async ( + options, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let date = moment(); + let startDate; + let endDate; + + startDate = date + .clone() + .add(0, forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(forecastNumber - 1, forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + // TODO:: only 200 per page what if the page is 1000. A loop needs to be created + let url = `${config.ZOHO_BOOK_BASE_URL}/bills?organization_id=${config.ORGANIZATION_ID}&due_date_start=${startDate}&due_date_end=${endDate}&sort_column=due_date`; + + resp = await axios.get(url, options); + + for (i = 0; i < forecastNumber; i++) { + startDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + const filteredBills = resp.data.bills.filter( + (item, index) => + item.due_date >= startDate && + item.due_date <= endDate && + (item.currency_code == 'NGN' || item.currency_code == 'USD') && + (item.status == 'open' || + item.status == 'overdue' || + item.status == 'partially_paid') + ); + + dollarClosingBalance = filteredBills.reduce(function (acc, obj) { + balance = obj.currency_code === 'USD' ? obj.balance : 0.0; + + return acc + balance; + }, 0); + + nairaClosingBalance = filteredBills.reduce(function (acc, obj) { + balance = + obj.currency_code === 'NGN' ? obj.balance : obj.balance * rate.latest; + + if (rate.old !== rate.latest) { + balance = (balance / rate.old) * rate.latest; + } + + return acc + balance; + }, 0); + + await createBillForecast( + userId, + nairaClosingBalance, + 0.0, + startDate, + forecastNumber, + forecastPeriod, + 'NGN' + ); + + await createBillForecast( + userId, + 0.0, + dollarClosingBalance, + startDate, + forecastNumber, + forecastPeriod, + 'USD' + ); + + filteredBills.reduce(async function (a, e) { + if (parseFloat(e.balance) > 0) { + if (rate.old !== rate.latest && e.currency_code === 'NGN') { + e.balance = (e.balance / rate.old) * rate.latest; + } + + const payload = { + userId, + billId: e.bill_id, + vendorId: e.vendor_id, + vendorName: e.vendor_name, + status: e.status, + invoiceNumber: e.bill_number, + refrenceNumber: e.reference_number, + date: e.date, + dueDate: e.due_date, + currencyCode: e.currency_code, + balance: e.balance, + naira: + e.currency_code === 'NGN' ? e.balance : e.balance * rate.latest, + dollar: e.currency_code === 'USD' ? e.balance : 0.0, + exchangeRate: e.exchange_rate, + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createBill({ payload }); + } + + return; + }, 0); + } +}; + +const getCustomerPayments = async ( + zohoAccessToken, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let currency_code; + let customerPayments = []; + let i = 1; + const TODAY_START = moment().startOf('day').format(); + const TODAY_END = moment().endOf('day').format(); + + options = { + headers: { + 'Content-Type': ['application/json'], + Authorization: 'Zoho-oauthtoken ' + zohoAccessToken, + }, + }; + + do { + let url = `${config.ZOHO_BOOK_BASE_URL}/customerpayments?organization_id=${config.ORGANIZATION_ID}&page=${i}`; + + resp = await axios.get(url, options); + + const filteredCustomerPayments = resp.data.customerpayments.filter( + (item, index) => { + const accountName = item.account_name; + const naira = /NGN/; + const dollar = /USD/; + + if (accountName.match(naira)) { + currency_code = 'NGN'; + } + + if (accountName.match(dollar)) { + currency_code = 'USD'; + } + return ( + item.unused_amount > 0 && + (currency_code === 'USD' || currency_code === 'NGN') + ); + } + ); + + filteredCustomerPayments.reduce(async function (a, e) { + customerPayments.push(e); + return e; + }, 0); + + ++i; + } while (resp.data.page_context.has_more_page); + + for (i = 0; i < customerPayments.length; i++) { + let e = customerPayments[i]; + const accountName = e.account_name; + const ngn = /NGN/; + const usd = /USD/; + let naira = accountName.match(ngn); + let dollar = accountName.match(usd); + + if (accountName.match(naira)) { + currency_code = 'NGN'; + } + + if (accountName.match(dollar)) { + currency_code = 'USD'; + } + + // let payload = { + // customerId: e.customer_id, + // userId, + // forecastNumber: forecastNumber, + // forecastPeriod: forecastPeriod, + // today_start: TODAY_START, + // today_end: TODAY_END, + // currencyCode: currency_code, + // }; + + // let customerPayment = await getCustomerPaymentByCustomerId({ payload }); + + // if (customerPayment) { + // await customerPayment.update({ + // amount: + // parseFloat(customerPayment.amount) + parseFloat(e.unused_amount), + // balance: + // parseFloat(customerPayment.unused_amount) + + // parseFloat(e.unused_amount), + // customerForcastbalance: + // parseFloat(customerPayment.customerForcastbalance) + + // parseFloat(e.unused_amount), + // }); + // continue; + // } + + payload = { + userId, + paymentId: e.payment_id, + customerId: e.customer_id, + customerName: e.customer_name, + billNumbers: e.bill_numbers, + referenceNumber: e.reference_number, + date: e.date, + currencyCode: currency_code, + amount: e.unused_amount, + balance: e.unused_amount, + saleForcastbalance: e.unused_amount, + exchangeRate: e.exchange_rate, + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createCustomerPayment({ + payload, + }); + } +}; + +const getVendorPayments = async ( + zohoAccessToken, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let vendorPayments = []; + let i = 1; + const TODAY_START = moment().startOf('day').format(); + const TODAY_END = moment().endOf('day').format(); + + options = { + headers: { + 'Content-Type': ['application/json'], + Authorization: 'Zoho-oauthtoken ' + zohoAccessToken, + }, + }; + + do { + let url = `${config.ZOHO_BOOK_BASE_URL}/vendorpayments?organization_id=${config.ORGANIZATION_ID}&page=${i}`; + + resp = await axios.get(url, options); + + const filteredVendorPayments = resp.data.vendorpayments.filter( + (item, index) => + item.balance > 0 && + (item.currency_code === 'USD' || item.currency_code === 'NGN') + ); + + filteredVendorPayments.reduce(async function (a, e) { + vendorPayments.push(e); + return e; + }, 0); + + ++i; + } while (resp.data.page_context.has_more_page); + + for (i = 0; i < vendorPayments.length; i++) { + let e = vendorPayments[i]; + // let payload = { + // vendorId: e.vendor_id, + // userId, + // forecastNumber: forecastNumber, + // forecastPeriod: forecastPeriod, + // today_start: TODAY_START, + // today_end: TODAY_END, + // currencyCode: e.currency_code, + // }; + + // let vendorPayment = await getVendorPaymentByVendorId({ payload }); + + // if (vendorPayment) { + // await vendorPayment.update({ + // amount: parseFloat(vendorPayment.amount) + parseFloat(e.balance), + // balance: parseFloat(vendorPayment.balance) + parseFloat(e.balance), + // purchaseForcastbalance: + // parseFloat(vendorPayment.purchaseForcastbalance) + + // parseFloat(e.balance), + // }); + // continue; + // } + + payload = { + userId, + paymentId: e.payment_id, + vendorId: e.vendor_id, + vendorName: e.vendor_name, + billNumbers: e.bill_numbers.length > 15 ? e.bill_numbers.substring(0, 15) + "..." : e.bill_numbers, + referenceNumber: e.reference_number, + date: e.date, + currencyCode: e.currency_code, + amount: e.balance, + balance: e.balance, + purchaseForcastbalance: e.balance, + exchangeRate: e.exchange_rate, + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createVendorPayment({ + payload, + }); + } +}; + +const getPurchaseOrder = async ( + options, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let date = moment(); + let startDate; + let endDate; + + startDate = date + .clone() + .add(0, forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(forecastNumber - 1, forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + let filteredPurchases = []; + let i = 1; + let payload; + + do { + let url = `${config.ZOHO_BOOK_BASE_URL}/purchaseorders?organization_id=${config.ORGANIZATION_ID}&delivery_date_end=${endDate}&sort_column=delivery_date&page=${i}`; + + resp = await axios.get(url, options); + + + if ( + Array.isArray(resp.data.purchaseorders) && + resp.data.purchaseorders.length > 0 + ) { + const filteredPreviousPurchases = resp.data.purchaseorders.filter( + (item, index) => item.delivery_date < startDate + ); + + const filteredCurrentPurchases = resp.data.purchaseorders.filter( + (item, index) => + item.delivery_date >= startDate && item.delivery_date <= endDate + ); + + filteredPreviousPurchases.reduce(async function (a, e) { + e.delivery_date = startDate; + filteredPurchases.push(e); + return e; + }, 0); + + filteredCurrentPurchases.reduce(async function (a, e) { + filteredPurchases.push(e); + return e; + }, 0); + } + + ++i; + } while (!resp.data.purchaseorders.length); + + for (i = 0; i < forecastNumber; i++) { + startDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + let newFilteredPurchases = filteredPurchases.filter( + (item, index) => + item.delivery_date >= startDate && + item.delivery_date <= endDate && + item.status == 'open' + ); + + dollarClosingBalance = newFilteredPurchases.reduce(function (acc, obj) { + balance = obj.currency_code === 'USD' ? obj.total : 0.0; + + return acc + balance; + }, 0); + + nairaClosingBalance = newFilteredPurchases.reduce(function (acc, obj) { + balance = + obj.currency_code === 'NGN' + ? obj.total + : obj.total * parseFloat(rate.latest).toFixed(2); + + if (rate.old !== rate.latest) { + balance = + (balance / parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + return acc + balance; + }, 0); + + await createPurchaseForecast( + userId, + parseFloat(nairaClosingBalance).toFixed(2), + 0.0, + startDate, + forecastNumber, + forecastPeriod, + 'NGN' + ); + + await createPurchaseForecast( + userId, + 0.0, + parseFloat(dollarClosingBalance).toFixed(2), + startDate, + forecastNumber, + forecastPeriod, + 'USD' + ); + + newFilteredPurchases.reduce(async function (a, e) { + if ( + parseFloat(e.total) > 0 && + (e.delivery_date >= startDate || e.delivery_date <= endDate) + ) { + if ( + parseFloat(rate.old).toFixed(2) !== + parseFloat(rate.latest).toFixed(2) && + e.currency_code === 'NGN' + ) { + e.total = + (parseFloat(e.total).toFixed(2) / parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + payload = { + userId, + purchaseOrderId: e.purchaseorder_id, + vendorId: e.vendor_id, + vendorName: e.vendor_name, + status: e.status, + purchaseOrderNumber: e.purchaseorder_number, + refrenceNumber: e.reference_number, + date: e.date, + deliveryDate: e.delivery_date, + currencyCode: e.currency_code, + total: e.total, + naira: + e.currency_code === 'NGN' + ? e.total + : e.total * parseFloat(rate.latest).toFixed(2), + dollar: e.currency_code === 'USD' ? e.total : 0.0, + balance: e.total, + exchangeRate: parseFloat(rate.latest).toFixed(2), + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createPurchase({ payload }); + } + return; + }, 0); + } +}; + +// const processPurchases = async ( +// purchases, +// vendorPayments, +// userId, +// forecastNumber, +// forecastPeriod +// ) => { +// const TODAY_START = moment().startOf('day').format(); +// const TODAY_END = moment().endOf('day').format(); + +// let nairaBalance = 0; +// let dollarBalance = 0; + +// for (i = 0; i < purchases.count; i++) { +// for (j = 0; j < vendorPayments.count; j++) { +// let purchase = purchases.rows[i]; +// let payment = vendorPayments.rows[j]; +// if ( +// purchase.vendorId == payment.vendorId && +// purchase.currencyCode == payment.currencyCode +// ) { +// let paymentMadeBalance = 0; +// let total = 0; + +// let purchaseMade = parseFloat(purchase.total); +// let vendorPayment = await VendorPayment.findOne({ +// where: { +// vendorId: payment.vendorId, +// }, +// }); +// let paymentMade = parseFloat(vendorPayment.balance); +// if (paymentMade >= purchaseMade) { +// paymentMadeBalance = paymentMade - purchaseMade; +// dollarBalance = +// payment.currencyCode == 'USD' +// ? dollarBalance + purchaseMade +// : dollarBalance; +// nairaBalance = +// payment.currencyCode == 'NGN' +// ? nairaBalance + purchaseMade +// : nairaBalance; + +// let payload = { +// userId: userId, +// forecastType: purchase.forecastType, +// currency: payment.currencyCode == 'NGN' ? 'NGN' : 'USD', +// today_start: TODAY_START, +// today_end: TODAY_END, +// }; + +// let purchaseForecast = await getPurchaseForecast({ payload }); + +// await purchaseForecast.update({ +// nairaClosingBalance: +// payment.currencyCode == 'NGN' +// ? parseFloat(purchaseForecast.nairaClosingBalance) - +// parseFloat(purchaseMade) +// : parseFloat(purchaseForecast.nairaClosingBalance), +// dollarClosingBalance: +// payment.currencyCode == 'USD' +// ? parseFloat(purchaseForecast.dollarClosingBalance) - +// parseFloat(purchaseMade) +// : parseFloat(purchaseForecast.dollarClosingBalance), +// }); +// } else { +// total = purchaseMade - paymentMade; + +// dollarBalance = +// payment.currencyCode == 'USD' +// ? dollarBalance + paymentMade +// : dollarBalance; +// nairaBalance = +// payment.currencyCode == 'NGN' +// ? nairaBalance + paymentMade +// : nairaBalance; +// let payload = { +// userId: userId, +// forecastType: purchase.forecastType, +// currency: payment.currencyCode == 'NGN' ? 'NGN' : 'USD', +// today_start: TODAY_START, +// today_end: TODAY_END, +// }; + +// let purchaseForecast = await getPurchaseForecast({ payload }); + +// await purchaseForecast.update({ +// nairaClosingBalance: +// payment.currencyCode == 'NGN' +// ? parseFloat(purchaseForecast.nairaClosingBalance) - +// parseFloat(purchaseMade) +// : parseFloat(purchaseForecast.nairaClosingBalance), +// dollarClosingBalance: +// payment.currencyCode == 'USD' +// ? parseFloat(purchaseForecast.dollarClosingBalance) - +// parseFloat(purchaseMade) +// : parseFloat(purchaseForecast.dollarClosingBalance), +// }); +// } +// await VendorPayment.update( +// { +// balance: paymentMadeBalance, +// }, +// { +// where: { +// vendorId: payment.vendorId, +// }, +// } +// ); + +// await Purchase.update( +// { +// balance: total, +// }, +// { +// where: { +// vendorId: payment.vendorId, +// purchaseOrderId: purchase.purchaseOrderId, +// }, +// } +// ); +// } +// } +// } +// }; + +const getSalesOrder = async ( + options, + forecastNumber, + forecastPeriod, + rate, + userId +) => { + let date = moment(); + let startDate; + let endDate; + + startDate = date + .clone() + .add(0, forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(forecastNumber - 1, forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + let filteredSales = []; + let i = 1; + + do { + let url = `${config.ZOHO_BOOK_BASE_URL}/salesorders?organization_id=${config.ORGANIZATION_ID}&shipment_date_start=${startDate}&shipment_date_end=${endDate}&sort_column=shipment_date&page=${i}`; + + resp = await axios.get(url, options); + + if ( + Array.isArray(resp.data.salesorders) && + resp.data.salesorders.length > 0 + ) { + const filteredPreviousSales = resp.data.salesorders.filter( + (item, index) => + item.shipment_date < startDate && + (item.status == 'open' || + item.status == 'overdue' || + item.status == 'partially_invoiced') + ); + + const filteredCurrentSales = resp.data.salesorders.filter( + (item, index) => + item.shipment_date >= startDate && + item.shipment_date <= endDate && + (item.status == 'open' || + item.status == 'overdue' || + item.status == 'partially_invoiced') + ); + + filteredPreviousSales.reduce(async function (a, e) { + e.shipment_date = startDate; + filteredSales.push(e); + return e; + }, 0); + + filteredCurrentSales.reduce(async function (a, e) { + filteredSales.push(e); + return e; + }, 0); + } + ++i; + } while (!resp.data.salesorders.length); + + for (i = 0; i < forecastNumber; i++) { + startDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = date + .clone() + .add(Math.abs(i), forecastPeriod) + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + + const newFilteredSales = filteredSales.filter( + (item, index) => + item.shipment_date >= startDate && + item.shipment_date <= endDate && + (item.status == 'open' || + item.status == 'overdue' || + item.status == 'partially_invoiced') + ); + + + dollarClosingBalance = newFilteredSales.reduce(function (acc, obj) { + balance = obj.currency_code === 'USD' ? obj.total : 0.0; + + return acc + balance; + }, 0); + + nairaClosingBalance = newFilteredSales.reduce(function (acc, obj) { + + balance = + obj.currency_code === 'NGN' + ? obj.total + : obj.total * parseFloat(rate.latest).toFixed(2); + + if (rate.old !== rate.latest) { + balance = + (balance / parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + return acc + balance; + }, 0); + + await createSaleForecast( + userId, + parseFloat(nairaClosingBalance).toFixed(2), + 0.0, + startDate, + forecastNumber, + forecastPeriod, + 'NGN' + ); + + await createSaleForecast( + userId, + 0.0, + parseFloat(dollarClosingBalance).toFixed(2), + startDate, + forecastNumber, + forecastPeriod, + 'USD' + ); + + newFilteredSales.reduce(async function (a, e) { + if (parseFloat(e.total) > 0) { + if ( + parseFloat(rate.old).toFixed(2) !== + parseFloat(rate.latest).toFixed(2) && + e.currency_code === 'NGN' + ) { + e.total = + (parseFloat(e.total).toFixed(2) / parseFloat(rate.old).toFixed(2)) * + parseFloat(rate.latest).toFixed(2); + } + + const payload = { + userId, + saleOrderId: e.salesorder_id, + customerId: e.customer_id, + customerName: e.customer_name, + status: e.status, + salesOrderNumber: e.salesorder_number, + refrenceNumber: e.reference_number, + date: e.date, + shipmentDate: e.shipment_date, + currencyCode: e.currency_code, + total: e.total, + naira: + e.currency_code === 'NGN' + ? e.total + : e.total * parseFloat(rate.latest).toFixed(2), + dollar: e.currency_code === 'USD' ? e.total : 0.0, + balance: e.total, + exchangeRate: parseFloat(rate.latest).toFixed(2), + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createSale({ payload }); + } + + return; + }, 0); + } +}; + +// const processSales = async ( +// sales, +// customerPayments, +// userId, +// forecastNumber, +// forecastPeriod +// ) => { +// const TODAY_START = moment().startOf('day').format(); +// const TODAY_END = moment().endOf('day').format(); +// let nairaBalance = 0; +// let dollarBalance = 0; + +// for (i = 0; i < sales.count; i++) { +// for (j = 0; j < customerPayments.count; j++) { +// let sale = sales.rows[i]; +// let payment = customerPayments.rows[j]; + +// if ( +// sale.customerId == payment.customerId && +// sale.currencyCode == payment.currencyCode +// ) { +// let paymentRecievedBalance = 0; +// let total = 0; + +// let saleRecieved = parseFloat(sale.total); +// let customerPayment = await CustomerPayment.findOne({ +// where: { +// customerId: payment.customerId, +// }, +// }); +// let paymentRecieved = parseFloat(customerPayment.balance); +// if (paymentRecieved >= saleRecieved) { +// paymentRecievedBalance = paymentRecieved - saleRecieved; +// dollarBalance = +// payment.currencyCode == 'USD' +// ? dollarBalance + saleRecieved +// : dollarBalance; +// nairaBalance = +// payment.currencyCode == 'NGN' +// ? nairaBalance + saleRecieved +// : nairaBalance; +// } else { +// total = saleRecieved - paymentRecieved; + +// dollarBalance = +// payment.currencyCode == 'USD' +// ? dollarBalance + paymentRecieved +// : dollarBalance; +// nairaBalance = +// payment.currencyCode == 'NGN' +// ? nairaBalance + paymentRecieved +// : nairaBalance; +// } +// await CustomerPayment.update( +// { +// balance: paymentRecievedBalance, +// }, +// { +// where: { +// customerId: payment.customerId, +// }, +// } +// ); + +// await Sale.update( +// { +// balance: total, +// }, +// { +// where: { +// customerId: payment.customerId, +// saleOrderId: sale.saleOrderId, +// }, +// } +// ); +// } +// } +// } + +// let payload = { +// userId: userId, +// forecastNumber: forecastNumber, +// forecastPeriod: forecastPeriod, +// currency: 'NGN', +// today_start: TODAY_START, +// today_end: TODAY_END, +// }; + +// let saleNairaForecast = await getSaleForecast({ payload }); + +// await saleNairaForecast.update({ +// nairaClosingBalance: +// parseFloat(saleNairaForecast.nairaClosingBalance) - +// parseFloat(nairaBalance), +// }); + +// payload = { +// userId: userId, +// forecastNumber: forecastNumber, +// forecastPeriod: forecastPeriod, +// currency: 'USD', +// today_start: TODAY_START, +// today_end: TODAY_END, +// }; + +// let saleDollarForecast = await getSaleForecast({ payload }); + +// await saleDollarForecast.update({ +// dollarClosingBalance: +// parseFloat(saleDollarForecast.dollarClosingBalance) - +// parseFloat(dollarBalance), +// }); +// }; + +const generateReportHandler = async (req, reply) => { + let statusCode = 400; + let result = { + status: false, + message: 'Could not generate report', + }; + + try { + let payload; + const { forecastNumber, forecastPeriod, download } = req.body; + const date = moment(); + const zohoAccessToken = req.body.zohoAccessToken; + const YESTERDAY_START = moment() + .subtract(1, 'days') + .startOf('day') + .format(); + const YESTERDAY_END = moment().subtract(1, 'days').endOf('day').format(); + const TODAY_START = moment().startOf('day').format(); + const TODAY_END = moment().endOf('day').format(); + let vendorPaymentForecastNairaClosingBalance = 0; + let vendorPaymentForecastDollarClosingBalance = 0; + let customerPaymentForecastNairaClosingBalance = 0; + let customerPaymentForecastDollarClosingBalance = 0; + + + let previousDayOpeningBalance; + + const userId = req.user.id; + + const options = { + headers: { + 'Content-Type': ['application/json'], + Authorization: 'Bearer ' + zohoAccessToken, + }, + }; + + let rate = await getZohoExchangeRateHandler( + zohoAccessToken, + forecastNumber, + forecastPeriod, + userId + ); + + if (!rate) { + return reply.code(400).send({ + status: false, + message: 'Could not fetch exchange rate', + }); + } + + payload = { + userId: userId, + forecastNumber: forecastNumber, + forecastPeriod: forecastPeriod, + today_start: TODAY_START, + today_end: TODAY_END, + }; + + // Get initial opening balance for a particular user + let initialOpeningBalance = await getInitialBalance({ payload }); + + // if user has not generated opening balance for the day then + if (!initialOpeningBalance) { + let prevOpeningBalData = { + yesterday_start: YESTERDAY_START, + yesterday_end: YESTERDAY_END, + }; + + // fetch previous day opening balance. + // previous day opening balance should have been populated from the CRON JOB. + previousDayOpeningBalance = await getPreviousDayOpeningBalance({ + prevOpeningBalData, + }); + + // if no previous day opneing balance throw and error + if (!previousDayOpeningBalance) { + return reply.code(400).send({ + status: false, + message: 'Could not fetch exchange rate for previous day', + }); + } + + let startingBalance = { + userId, + openingBalance: + parseFloat(previousDayOpeningBalance.naira) + + parseFloat(previousDayOpeningBalance.dollar) * + parseFloat(rate.latest).toFixed(2), + nairaOpeningBalance: parseFloat(previousDayOpeningBalance.naira), + dollarOpeningBalance: parseFloat(previousDayOpeningBalance.dollar), + rate: rate.latest, + forecastType: `${forecastNumber} ${forecastPeriod}`, + }; + + await createInitialBalance({ startingBalance }); + + let payload = { + userId: userId, + forecastNumber: forecastNumber, + forecastPeriod: forecastPeriod, + today_start: TODAY_START, + today_end: TODAY_END, + }; + + initialOpeningBalance = await getInitialBalance({ payload }); + } + + let customerPayments = await fetchAllCustomerPayments({ payload }); + + let vendorPayments = await fetchAllVendorPayments({ payload }); + + let invoices = await fetchAllInvoice({ payload }); + + let bills = await fetchAllBill({ payload }); + + let purchases = await fetchAllPurchase({ payload }); + + let sales = await fetchAllSale({ payload }); + + let purchaseForecasts = await fetchAllPurchaseForecast({ payload }); + + let saleForecasts = await fetchAllSaleForecast({ payload }); + + // get invoice forecast where forecastType + let invoiceForecasts = await fetchAllInvoiceForecast({ payload }); + + let billForecasts = await fetchAllBillForecast({ payload }); + + if ( + !saleForecasts.count && + !invoiceForecasts.count && + !purchaseForecasts.count && + !billForecasts.count && + !sales.count && + !invoices.count && + !purchases.count && + !bills.count && + !customerPayments.count && + !vendorPayments.count + ) { + await getCustomerPayments( + zohoAccessToken, + forecastNumber, + forecastPeriod, + rate, + userId + ); + + await getVendorPayments( + zohoAccessToken, + forecastNumber, + forecastPeriod, + rate, + userId + ); + + await getSalesOrder( + options, + forecastNumber, + forecastPeriod, + rate, + userId + ); + + await getPurchaseOrder( + options, + forecastNumber, + forecastPeriod, + rate, + userId + ); + + await getInvoice(options, forecastNumber, forecastPeriod, rate, userId); + + await getBill(options, forecastNumber, forecastPeriod, rate, userId); + + customerPayments = await fetchAllCustomerPayments({ payload }); + + vendorPayments = await fetchAllVendorPayments({ payload }); + + purchases = await fetchAllPurchase({ payload }); + + sales = await fetchAllSale({ payload }); + + invoices = await fetchAllInvoice({ payload }); + + bills = await fetchAllBill({ payload }); + + purchaseForecasts = await fetchAllPurchaseForecast({ payload }); + + saleForecasts = await fetchAllSaleForecast({ payload }); + + // get invoice forecast where forecastType + invoiceForecasts = await fetchAllInvoiceForecast({ payload }); + + billForecasts = await fetchAllBillForecast({ payload }); + + // await processSales( + // sales, + // customerPayments, + // userId, + // forecastNumber, + // forecastPeriod + // ); + + // await processPurchases( + // purchases, + // vendorPayments, + // userId, + // forecastNumber, + // forecastPeriod + // ); + } + + + + let check = date.clone().add(0, forecastPeriod).startOf(forecastPeriod); + let month = check.format('MMMM'); + + let closingBalances = []; + let nairaOpeningBalance = parseFloat(initialOpeningBalance.openingBalance); + let dollarOpeningBalance = parseFloat( + initialOpeningBalance.dollarOpeningBalance + ); + let openingBalances = [ + { + month: month, + amount: parseFloat(initialOpeningBalance.openingBalance), + currency: 'NGN', + date: check, + }, + { + month: month, + amount: parseFloat(initialOpeningBalance.dollarOpeningBalance), + currency: 'USD', + date: check, + }, + ]; + + for (i = 0; i < invoiceForecasts.rows.length - 2; i++) { + //INFLOW TOTAL + let invoiceForeacastClosingBalance = + invoiceForecasts.rows[i].currency === 'NGN' + ? invoiceForecasts.rows[i].nairaClosingBalance + : invoiceForecasts.rows[i].dollarClosingBalance; + let saleForecastClosingBalance = + saleForecasts.rows[i].currency === 'NGN' + ? saleForecasts.rows[i].nairaClosingBalance + : saleForecasts.rows[i].dollarClosingBalance; + + // OUTFLOW TOTAL + let billForeacastClosingBalance = + billForecasts.rows[i].currency === 'NGN' + ? billForecasts.rows[i].nairaClosingBalance + : billForecasts.rows[i].dollarClosingBalance; + let purchaseForecastClosingBalance = + purchaseForecasts.rows[i].currency === 'NGN' + ? purchaseForecasts.rows[i].nairaClosingBalance + : purchaseForecasts.rows[i].dollarClosingBalance; + + let openingBalDate = moment( + invoiceForecasts.rows[i + 2].month, + 'YYYY-MM-DD' + ); + let opneingBalMonth = openingBalDate.format('MMMM'); + + let closingBalDate = moment(invoiceForecasts.rows[i].month, 'YYYY-MM-DD'); + let closingBalMonth = closingBalDate.format('MMMM'); + + // Get monthly opening and closing balances and keep them in an array + if (invoiceForecasts.rows[i].currency === 'NGN') { + openingInflowBalanceNairaResult = 0; + openingOutflowBalanceNairaResult = 0; + openingInflowBalanceNairaResult = 0; + openingOutflowBalanceNairaResult = 0; + // loop vendor payment and customer payment then sum up naira values + if(i == 0){ + for (const [rowNum, inputData] of customerPayments.rows.entries()) { + + if(customerPayments.rows[rowNum].currencyCode === 'NGN'){ + + customerPaymentForecastNairaClosingBalance = customerPaymentForecastNairaClosingBalance + parseFloat(inputData.amount) + } + + if(customerPayments.rows[rowNum].currencyCode == 'USD'){ + customerPaymentForecastNairaClosingBalance += parseFloat(inputData.amount) * rate.latest; + customerPaymentForecastDollarClosingBalance = customerPaymentForecastDollarClosingBalance + parseFloat(inputData.amount) + } + } + + for (const [rowNum, inputData] of vendorPayments.rows.entries()) { + + if(vendorPayments.rows[rowNum].currencyCode === 'NGN'){ + + vendorPaymentForecastNairaClosingBalance = vendorPaymentForecastNairaClosingBalance + parseFloat(inputData.amount) + } + + if(vendorPayments.rows[rowNum].currencyCode == 'USD'){ + vendorPaymentForecastNairaClosingBalance += parseFloat(inputData.amount) * rate.latest; + vendorPaymentForecastDollarClosingBalance = vendorPaymentForecastDollarClosingBalance + parseFloat(inputData.amount) + } + } + + let openingInflowBalanceNaira = parseFloat(invoiceForeacastClosingBalance) + parseFloat(saleForecastClosingBalance); + openingInflowBalanceNairaResult = openingInflowBalanceNaira - parseFloat(customerPaymentForecastNairaClosingBalance); + + let openingOutflowBalanceNaira = parseFloat(purchaseForecastClosingBalance) + parseFloat(billForeacastClosingBalance); + openingOutflowBalanceNairaResult = openingOutflowBalanceNaira - parseFloat(vendorPaymentForecastNairaClosingBalance); + + }else{ + let openingInflowBalanceNaira = parseFloat(invoiceForeacastClosingBalance) + parseFloat(saleForecastClosingBalance); + openingInflowBalanceNairaResult = openingInflowBalanceNaira; + + let openingOutflowBalanceNaira = parseFloat(purchaseForecastClosingBalance) + parseFloat(billForeacastClosingBalance); + openingOutflowBalanceNairaResult = openingOutflowBalanceNaira; + } + + nairaOpeningBalance += openingInflowBalanceNairaResult - openingOutflowBalanceNairaResult ; + + openingBalances.push({ + month: opneingBalMonth, + amount: nairaOpeningBalance, + currency: invoiceForecasts.rows[i].currency, + date: openingBalDate, + }); + closingBalances.push({ + month: closingBalMonth, + amount: nairaOpeningBalance, + currency: invoiceForecasts.rows[i].currency, + date: closingBalDate, + }); + } else { + let openingInflowBalanceDollar = parseFloat(invoiceForeacastClosingBalance) + parseFloat(saleForecastClosingBalance); + let openingInflowBalanceDollarResult = openingInflowBalanceDollar - parseFloat(customerPaymentForecastDollarClosingBalance); + + let openingOutflowBalanceDollar = parseFloat(purchaseForecastClosingBalance) + parseFloat(billForeacastClosingBalance); + let openingOutflowBalanceDollarResult = openingOutflowBalanceDollar - parseFloat(vendorPaymentForecastDollarClosingBalance); + + dollarOpeningBalance += openingInflowBalanceDollarResult - openingOutflowBalanceDollarResult; + + openingBalances.push({ + month: opneingBalMonth, + amount: dollarOpeningBalance, + currency: invoiceForecasts.rows[i].currency, + date: openingBalDate, + }); + closingBalances.push({ + month: closingBalMonth, + amount: dollarOpeningBalance, + currency: invoiceForecasts.rows[i].currency, + date: closingBalDate, + }); + } + } + + // Last Closing balance + closingBalDate = date + .clone() + .add(Math.abs(forecastNumber - 1), forecastPeriod) + .startOf(forecastPeriod); + closingBalMonth = closingBalDate.format('MMMM'); + + // get last item from opening balance + let dollarLastOpeningBalance = openingBalances.at(-1); + let nairaLastOpeningBalance = openingBalances.at(-2); + + // get last item from invoiceForecasts + let dollarLastInvoice = invoiceForecasts.rows.at(-1); + let nairaLastInvoice = invoiceForecasts.rows.at(-2); + + // get last item from salesForecast + let dollarLastSale = saleForecasts.rows.at(-1); + let nairaLastSale = saleForecasts.rows.at(-1); + + // get last item from purchaseForecasts + let dollarLastPurchase = purchaseForecasts.rows.at(-1); + let nairaLastPurchase = purchaseForecasts.rows.at(-1); + + + // get last item from billForecasts + let dollarLastBill = billForecasts.rows.at(-1); + let nairaLastBill = billForecasts.rows.at(-2); + + lastTotalInflowDollar = parseFloat(dollarLastOpeningBalance.amount) + + parseFloat(dollarLastInvoice.dollarClosingBalance) + + parseFloat(dollarLastSale.dollarClosingBalance); + + lastTotalOutflowDollar = parseFloat(dollarLastBill.dollarClosingBalance) + + parseFloat(dollarLastPurchase.dollarClosingBalance); + + lastTotalInflowNaira = parseFloat(nairaLastOpeningBalance.amount) + + parseFloat(nairaLastInvoice.nairaClosingBalance) + + parseFloat(nairaLastSale.nairaClosingBalance); + + lastTotalOutflowNaira = parseFloat(nairaLastBill.nairaClosingBalance) + + parseFloat(nairaLastPurchase.nairaClosingBalance); + + + let lastNairaClosingBalance =lastTotalInflowNaira - lastTotalOutflowNaira; + let lastDollarClosingBalance =lastTotalInflowDollar - lastTotalOutflowDollar; + + + // then push to closing Balance + closingBalances.push({ + month: closingBalMonth, + amount: lastNairaClosingBalance, + currency: 'NGN', + date: closingBalDate, + }); + closingBalances.push({ + month: closingBalMonth, + amount: lastDollarClosingBalance, + currency: 'USD', + date: closingBalDate, + }); + + // loop through opening balance to get cash inflow from invoiced sales total and cash outflow on currennt trade payables total + for (j = 0; j < openingBalances.length / 2; j++) { + startDate = moment(openingBalances[j * 2].date, 'YYYY-MM-DD') + .startOf(forecastPeriod) + .format('YYYY-MM-DD'); + endDate = moment(openingBalances[j * 2].date, 'YYYY-MM-DD') + .endOf(forecastPeriod) + .format('YYYY-MM-DD'); + } + + // Allows you to generate and download report + if (download) { + const workbook = new ExcelJS.Workbook(); + workbook.calcProperties.fullCalcOnLoad = true; + const sheet = workbook.addWorksheet('My Sheet'); + + const monthRow = sheet.getRow(1); + const currencyRow = sheet.getRow(2); + const openingBalanceRow = sheet.getRow(3); + const cashInflow = sheet.getRow(6); + + openingBalanceRow.getCell(1).value = 'Opening Balance'; + cashInflow.getCell(1).value = 'CASH INFLOWS'; + + for (const [rowNum, inputData] of openingBalances.entries()) { + monthRow.getCell(rowNum + 3).value = inputData.month; + currencyRow.getCell(rowNum + 3).value = inputData.currency; + openingBalanceRow.getCell(rowNum + 3).value = inputData.amount; + + monthRow.commit(); + currencyRow.commit(); + openingBalanceRow.commit(); + } + let openingBalanceLength = openingBalances.length + 1; + monthRow.getCell(openingBalanceLength + 2).value = 'Total'; + monthRow.getCell(openingBalanceLength + 3).value = 'Total'; + monthRow.getCell(openingBalanceLength + 5).value = 'Rate'; + + currencyRow.getCell(openingBalanceLength + 2).value = 'NGN'; + currencyRow.getCell(openingBalanceLength + 3).value = 'USD Exposure'; + currencyRow.getCell(openingBalanceLength + 5).value = rate.latest; + + openingBalanceRow.getCell(openingBalanceLength + 2).value = + initialOpeningBalance.openingBalance; + openingBalanceRow.getCell(openingBalanceLength + 3).value = + initialOpeningBalance.dollarOpeningBalance; + + monthRow.commit(); + currencyRow.commit(); + openingBalanceRow.commit(); + + let cashInflowFromInvoiced; + let cashInflowFromPendingOrders; + let totalCashInflowFromOperatingActivities; + let cashInflowFromCustomerPayments; + + let cashOutflow; + let cashOutflowFromPurchase; + + let endOfInvoice = (await invoices.count) + 7; + let endOfSale = endOfInvoice + (await sales.count) + 2; + let endOfCustomerPayment = endOfSale + (await customerPayments.count) + 2; + // end of customer payment + let endOfBill = (await bills.count) + endOfCustomerPayment + 4; + let endOfPurchase = (await purchases.count) + endOfBill + 2; + let endOfVendorPayment = (await vendorPayments.count) + endOfPurchase + 2; + // end of vendor payment + + for (const [rowNum, inputData] of invoices.rows.entries()) { + const rowX = sheet.getRow(rowNum + 7); + + inputData.dueDate = moment(inputData.dueDate, 'YYYY-MM-DD').format( + 'MMMM' + ); + + for (i = 1; i < invoiceForecasts.rows.length + 1; i++) { + month = moment( + invoiceForecasts.rows[i - 1].month, + 'YYYY-MM-DD' + ).format('MMMM'); + + if ( + inputData.dueDate === month && + inputData.currencyCode.toLowerCase() === + invoiceForecasts.rows[i - 1].currency.toLowerCase() + ) { + if (inputData.currencyCode.toLowerCase() === 'NGN'.toLowerCase()) { + rowX.getCell(i + 2).value = inputData.balance; + } + + if (inputData.currencyCode.toLowerCase() === 'USD'.toLowerCase()) { + rowX.getCell(i + 1).value = inputData.naira; + rowX.getCell(i + 2).value = inputData.balance; + } + } + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.customerName; + rowX.getCell(2).value = inputData.invoiceNumber; + }); + + rowX.commit(); + } + cashInflowFromInvoiced = sheet.getRow(endOfInvoice); + + cashInflowFromInvoiced.getCell(1).value = + 'Cash inflow from invoiced sales'; + + let totalNairaClosingBalance = 0.0; + let totalDollarClosingBalance = 0.0; + let totalNairaClosingBalanceSales = 0.0; + let totalDollarClosingBalanceSales = 0.0; + let totalNairaClosingBalancePurchase = 0.0; + let totalDollarClosingBalancePurchase = 0.0; + + for (const [rowNum, inputData] of invoiceForecasts.rows.entries()) { + if (invoiceForecasts.rows[rowNum].currency === 'NGN') { + totalNairaClosingBalance = + totalNairaClosingBalance + + parseFloat(inputData.nairaClosingBalance); + cashInflowFromInvoiced.getCell(rowNum + 3).value = + inputData.nairaClosingBalance; + } else { + totalDollarClosingBalance = + totalDollarClosingBalance + + parseFloat(inputData.dollarClosingBalance); + cashInflowFromInvoiced.getCell(rowNum + 3).value = + inputData.dollarClosingBalance; + } + + cashInflowFromInvoiced.commit(); + } + + // total invoice forecast for dollar and naira + let invoiceForecastsLength = invoiceForecasts.rows.length; + cashInflowFromInvoiced.getCell(invoiceForecastsLength + 3).value = + totalNairaClosingBalance; + cashInflowFromInvoiced.getCell(invoiceForecastsLength + 4).value = + totalDollarClosingBalance; + + cashInflowFromInvoiced.commit(); + // totalCashInflowFromOperatingActivities.commit(); + + for (const [rowNum, inputData] of sales.rows.entries()) { + const rowX = sheet.getRow(rowNum + endOfInvoice + 2); + + inputData.shipmentDate = moment( + inputData.shipmentDate, + 'YYYY-MM-DD' + ).format('MMMM'); + + for (i = 1; i < saleForecasts.rows.length + 1; i++) { + month = moment(saleForecasts.rows[i - 1].month, 'YYYY-MM-DD').format( + 'MMMM' + ); + + if ( + inputData.shipmentDate === month && + inputData.currencyCode.toLowerCase() === + invoiceForecasts.rows[i - 1].currency.toLowerCase() + ) { + if (inputData.currencyCode.toLowerCase() === 'NGN'.toLowerCase()) { + rowX.getCell(i + 2).value = inputData.balance; + } + + if (inputData.currencyCode.toLowerCase() === 'USD'.toLowerCase()) { + rowX.getCell(i + 1).value = inputData.naira; + rowX.getCell(i + 2).value = inputData.balance; + } + } + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.customerName; + rowX.getCell(2).value = inputData.salesOrderNumber; + }); + + rowX.commit(); + } + cashInflowFromPendingOrders = sheet.getRow(endOfSale); + + cashInflowFromPendingOrders.getCell(1).value = + 'Cash inflow from pending orders'; + + for (const [rowNum, inputData] of saleForecasts.rows.entries()) { + if (saleForecasts.rows[rowNum].currency === 'NGN') { + totalNairaClosingBalanceSales = + totalNairaClosingBalanceSales + + parseFloat(inputData.nairaClosingBalance); + cashInflowFromPendingOrders.getCell(rowNum + 3).value = + inputData.nairaClosingBalance; + } else { + totalDollarClosingBalanceSales = + totalDollarClosingBalanceSales + + parseFloat(inputData.dollarClosingBalance); + cashInflowFromPendingOrders.getCell(rowNum + 3).value = + inputData.dollarClosingBalance; + } + + cashInflowFromPendingOrders.commit(); + } + + for (const [rowNum, inputData] of customerPayments.rows.entries()) { + const rowX = sheet.getRow(rowNum + endOfSale + 2); + + if(customerPayments.rows[rowNum].currencyCode === 'NGN'){ + rowX.getCell(3).value = inputData.amount; + } + + if(customerPayments.rows[rowNum].currencyCode === 'USD'){ + rowX.getCell(3).value = inputData.amount * rate.latest; + rowX.getCell(4).value = inputData.amount; + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.customerName; + rowX.getCell(2).value = inputData.paymentId; + }); + + rowX.commit(); + } + + cashInflowFromCustomerPayments = sheet.getRow(endOfCustomerPayment); + + cashInflowFromCustomerPayments.getCell(1).value = + 'Customers Payment'; + + totalCashInflowFromOperatingActivities = sheet.getRow(endOfCustomerPayment + 1); + cashOutflow = sheet.getRow(endOfCustomerPayment + 3); + totalCashInflowFromOperatingActivities.getCell(1).value = + 'Total Cash Inflows from Operating Activties'; + cashOutflow.getCell(1).value = 'CASH OUTFLOWS'; + + for (let i = 0; i < saleForecasts.rows.length; i++) { + for (let j = 0; j < invoiceForecasts.rows.length; j++) { + + if(i <= 1){ + if ( + saleForecasts.rows[i].currency === 'NGN' && + invoiceForecasts.rows[i].currency === 'NGN' + ) { + + totalCashInflowFromOperatingActivities.getCell(i + 3).value = parseFloat(saleForecasts.rows[i].nairaClosingBalance) + + parseFloat(invoiceForecasts.rows[i].nairaClosingBalance) - parseFloat(customerPaymentForecastNairaClosingBalance); + } + + if ( + saleForecasts.rows[i].currency === 'USD' && + invoiceForecasts.rows[i].currency === 'USD' + ) { + + totalCashInflowFromOperatingActivities.getCell(i + 3).value = parseFloat(saleForecasts.rows[i].dollarClosingBalance) + + parseFloat(invoiceForecasts.rows[i].dollarClosingBalance) - parseFloat(customerPaymentForecastDollarClosingBalance); + } + }else{ + + if ( + saleForecasts.rows[i].currency === 'NGN' && + invoiceForecasts.rows[i].currency === 'NGN' + ) { + totalCashInflowFromOperatingActivities.getCell(i + 3).value = + parseFloat(saleForecasts.rows[i].nairaClosingBalance) + + parseFloat(invoiceForecasts.rows[i].nairaClosingBalance); + } + + if ( + saleForecasts.rows[i].currency === 'USD' && + invoiceForecasts.rows[i].currency === 'USD' + ) { + totalCashInflowFromOperatingActivities.getCell(i + 3).value = + parseFloat(saleForecasts.rows[i].dollarClosingBalance) + + parseFloat(invoiceForecasts.rows[i].dollarClosingBalance); + } + } + + } + } + // total invoice forecast for dollar and naira + let saleForecastsLength = saleForecasts.rows.length; + + cashInflowFromPendingOrders.getCell(saleForecastsLength + 3).value = + totalNairaClosingBalanceSales; + + cashInflowFromPendingOrders.getCell(saleForecastsLength + 4).value = + totalDollarClosingBalanceSales; + + totalCashInflowFromOperatingActivities.getCell( + saleForecastsLength + 3 + ).value = totalNairaClosingBalance + totalNairaClosingBalanceSales - parseFloat(customerPaymentForecastNairaClosingBalance); + totalCashInflowFromOperatingActivities.getCell( + saleForecastsLength + 4 + ).value = totalDollarClosingBalance + totalDollarClosingBalanceSales - parseFloat(customerPaymentForecastDollarClosingBalance); + + cashInflowFromPendingOrders.commit(); + totalCashInflowFromOperatingActivities.commit(); + + newArray = []; + for (const [rowNum, inputData] of bills.rows.entries()) { + const rowX = sheet.getRow(rowNum + endOfCustomerPayment + 4); + + inputData.dueDate = moment(inputData.dueDate, 'YYYY-MM-DD').format( + 'MMMM' + ); + + for (i = 1; i < invoiceForecasts.rows.length + 1; i++) { + month = moment( + invoiceForecasts.rows[i - 1].month, + 'YYYY-MM-DD' + ).format('MMMM'); + if ( + inputData.dueDate === month && + inputData.currencyCode.toLowerCase() === + invoiceForecasts.rows[i - 1].currency.toLowerCase() + ) { + if (inputData.currencyCode.toLowerCase() === 'NGN'.toLowerCase()) { + rowX.getCell(i + 2).value = inputData.balance; + } + + if (inputData.currencyCode.toLowerCase() === 'USD'.toLowerCase()) { + rowX.getCell(i + 1).value = inputData.naira; + rowX.getCell(i + 2).value = inputData.balance; + } + } + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.vendorName; + rowX.getCell(2).value = inputData.refrenceNumber; + }); + + rowX.commit(); + } + + cashOutflowsOnCurrentTradePayables = sheet.getRow(endOfBill); + cashOutflowsOnCurrentTradePayables.getCell(1).value = + 'Cash Outflows on Current Trade Payables'; + + let totalNairaCashOutflowsOnCurrentTradePayables = 0.0; + let totalDollarCashOutflowsOnCurrentTradePayables = 0.0; + + for (const [rowNum, inputData] of billForecasts.rows.entries()) { + if (billForecasts.rows[rowNum].currency === 'NGN') { + totalNairaCashOutflowsOnCurrentTradePayables += parseFloat( + inputData.nairaClosingBalance + ); + cashOutflowsOnCurrentTradePayables.getCell(rowNum + 3).value = + inputData.nairaClosingBalance; + } else { + totalDollarCashOutflowsOnCurrentTradePayables += parseFloat( + inputData.dollarClosingBalance + ); + // totalDollarCashOutflows += parseFloat(inputData.dollarClosingBalance); + cashOutflowsOnCurrentTradePayables.getCell(rowNum + 3).value = + inputData.dollarClosingBalance; + // totalCashOutflows.getCell(rowNum + 3).value = + // inputData.dollarClosingBalance; + } + + cashOutflowsOnCurrentTradePayables.commit(); + } + + // total bill forecast for naira and dollar + let billForecastsLength = billForecasts.rows.length; + cashOutflowsOnCurrentTradePayables.getCell( + billForecastsLength + 3 + ).value = totalNairaCashOutflowsOnCurrentTradePayables; + + cashOutflowsOnCurrentTradePayables.getCell( + billForecastsLength + 4 + ).value = totalDollarCashOutflowsOnCurrentTradePayables; + + // totalCashOutflows.getCell(billForecastsLength + 4).value = + // totalDollarCashOutflows; + + // totalCashOutflows.getCell(billForecastsLength + 3).value = + // totalNairaCashOutflows; + + cashOutflowsOnCurrentTradePayables.commit(); + // totalCashOutflows.commit(); + + for (const [rowNum, inputData] of purchases.rows.entries()) { + const rowX = sheet.getRow(rowNum + endOfBill + 2); + + inputData.deliveryDate = moment( + inputData.deliveryDate, + 'YYYY-MM-DD' + ).format('MMMM'); + + for (i = 1; i < purchaseForecasts.rows.length + 1; i++) { + month = moment( + purchaseForecasts.rows[i - 1].month, + 'YYYY-MM-DD' + ).format('MMMM'); + + if ( + inputData.deliveryDate === month && + inputData.currencyCode.toLowerCase() === + purchaseForecasts.rows[i - 1].currency.toLowerCase() + ) { + if (inputData.currencyCode.toLowerCase() === 'NGN'.toLowerCase()) { + rowX.getCell(i + 2).value = inputData.balance; + } + + if (inputData.currencyCode.toLowerCase() === 'USD'.toLowerCase()) { + rowX.getCell(i + 1).value = inputData.naira; + rowX.getCell(i + 2).value = inputData.balance; + } + } + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.vendorName; + rowX.getCell(2).value = inputData.purchaseOrderNumber; + }); + + rowX.commit(); + } + + cashOutflowFromPurchase = sheet.getRow(endOfPurchase); + + cashOutflowFromPurchase.getCell(1).value = 'Cash outflow from purchase'; + + totalCashOutflows = sheet.getRow(endOfVendorPayment + 2); + netWorkingCapital = sheet.getRow(endOfVendorPayment + 4); + + let totalNairaCashOutflows = 0.0; + let totalDollarCashOutflows = 0.0; + + for (const [rowNum, inputData] of purchaseForecasts.rows.entries()) { + if (purchaseForecasts.rows[rowNum].currency === 'NGN') { + totalNairaClosingBalancePurchase = + totalNairaClosingBalancePurchase + + parseFloat(inputData.nairaClosingBalance); + cashOutflowFromPurchase.getCell(rowNum + 3).value = + inputData.nairaClosingBalance; + + totalNairaCashOutflows += parseFloat(inputData.nairaClosingBalance); + + totalCashOutflows.getCell(rowNum + 3).value = + inputData.nairaClosingBalance; + // totalCashInflowFromOperatingActivities.getCell(rowNum + 3).value = + // inputData.nairaClosingBalance; + } else { + totalDollarClosingBalancePurchase = + totalDollarClosingBalancePurchase + + parseFloat(inputData.dollarClosingBalance); + cashOutflowFromPurchase.getCell(rowNum + 3).value = + inputData.dollarClosingBalance; + + totalDollarCashOutflows += parseFloat(inputData.dollarClosingBalance); + + totalCashOutflows.getCell(rowNum + 3).value = + inputData.dollarClosingBalance; + + // totalCashInflowFromOperatingActivities.getCell(rowNum + 3).value = + // inputData.dollarClosingBalance; + } + + cashOutflowFromPurchase.commit(); + } + + for (let i = 0; i < purchaseForecasts.rows.length; i++) { + for (let j = 0; j < billForecasts.rows.length; j++) { + if(i <= 1) { + if ( + purchaseForecasts.rows[i].currency === 'NGN' && + billForecasts.rows[i].currency === 'NGN' + ) { + totalCashOutflows.getCell(i + 3).value = + parseFloat(purchaseForecasts.rows[i].nairaClosingBalance) + + parseFloat(billForecasts.rows[i].nairaClosingBalance) - parseFloat(vendorPaymentForecastNairaClosingBalance); + } + + if ( + purchaseForecasts.rows[i].currency === 'USD' && + billForecasts.rows[i].currency === 'USD' + ) { + totalCashOutflows.getCell(i + 3).value = + parseFloat(purchaseForecasts.rows[i].dollarClosingBalance) + + parseFloat(billForecasts.rows[i].dollarClosingBalance) - parseFloat(vendorPaymentForecastDollarClosingBalance); + } + }else{ + if ( + purchaseForecasts.rows[i].currency === 'NGN' && + billForecasts.rows[i].currency === 'NGN' + ) { + totalCashOutflows.getCell(i + 3).value = + parseFloat(purchaseForecasts.rows[i].nairaClosingBalance) + + parseFloat(billForecasts.rows[i].nairaClosingBalance); + } + + if ( + purchaseForecasts.rows[i].currency === 'USD' && + billForecasts.rows[i].currency === 'USD' + ) { + totalCashOutflows.getCell(i + 3).value = + parseFloat(purchaseForecasts.rows[i].dollarClosingBalance) + + parseFloat(billForecasts.rows[i].dollarClosingBalance); + } + } + } + } + + for (const [rowNum, inputData] of vendorPayments.rows.entries()) { + const rowX = sheet.getRow(rowNum + endOfPurchase + 2); + + if(vendorPayments.rows[rowNum].currencyCode === 'NGN'){ + rowX.getCell(3).value = inputData.amount; + } + + if(vendorPayments.rows[rowNum].currencyCode === 'USD'){ + rowX.getCell(3).value = inputData.amount * rate.latest; + rowX.getCell(4).value = inputData.amount; + } + + sheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + rowX.getCell(1).value = inputData.vendorName; + rowX.getCell(2).value = inputData.paymentId; + }); + + rowX.commit(); + } + + cashOutflowFromVendorPayments = sheet.getRow(endOfVendorPayment); + + cashOutflowFromVendorPayments.getCell(1).value = + 'Vendors Payment'; + + totalCashOutflows.getCell(1).value = 'Total Cash Outflows'; + + + // total invoice forecast for dollar and naira + let purchaseForecastsLength = purchaseForecasts.rows.length; + + cashOutflowFromPurchase.getCell(purchaseForecastsLength + 3).value = + totalNairaClosingBalancePurchase; + + cashOutflowFromPurchase.getCell(purchaseForecastsLength + 4).value = + totalDollarClosingBalancePurchase; + + totalCashOutflows.getCell(purchaseForecastsLength + 3).value = + totalNairaCashOutflowsOnCurrentTradePayables + + totalNairaClosingBalancePurchase - parseFloat(vendorPaymentForecastNairaClosingBalance); + totalCashOutflows.getCell(purchaseForecastsLength + 4).value = + totalDollarCashOutflowsOnCurrentTradePayables + + totalDollarClosingBalancePurchase - parseFloat(vendorPaymentForecastDollarClosingBalance); + + cashOutflowFromPurchase.commit(); + totalCashOutflows.commit(); + + netWorkingCapital.getCell(1).value = 'Closing Balance'; + const closingBalanceRow = netWorkingCapital; + + for (const [rowNum, inputData] of closingBalances.entries()) { + closingBalanceRow.getCell(rowNum + 3).value = inputData.amount; + + closingBalanceRow.commit(); + } + + // nairaNetWorkingCapital + let totalNairaNetWorkingCapital = + (parseFloat(initialOpeningBalance.openingBalance) + + parseFloat(totalNairaClosingBalance + totalNairaClosingBalanceSales) - + parseFloat(customerPaymentForecastNairaClosingBalance)) - + (parseFloat( + totalNairaCashOutflowsOnCurrentTradePayables + + totalNairaClosingBalancePurchase - vendorPaymentForecastNairaClosingBalance + )); + // dollarNetWorkingCapital + let totalDollarNetWorkingCapital = + (parseFloat(initialOpeningBalance.dollarOpeningBalance) + + parseFloat(totalDollarClosingBalance + totalDollarClosingBalanceSales) - + parseFloat(customerPaymentForecastDollarClosingBalance)) - + parseFloat( + totalDollarCashOutflowsOnCurrentTradePayables + + totalDollarClosingBalancePurchase - vendorPaymentForecastDollarClosingBalance + ); + + netWorkingCapital.getCell(closingBalances.length + 3).value = + totalNairaNetWorkingCapital; + netWorkingCapital.getCell(closingBalances.length + 4).value = + totalDollarNetWorkingCapital; + + statusCode = 200; + result = await workbook.xlsx.writeBuffer(); + } else { + let totalInvoiceNairaCashInflow = 0.0; + let totalInvoiceDollarCashInflow = 0.0; + let totalSaleNairaCashInflow = 0.0; + let totalSaleDollarCashInflow = 0.0; + + let totalBillNairaCashOutflow = 0.0; + let totalBillDollarCashOutflow = 0.0; + let totalPurchaseNairaCashOutflow = 0.0; + let totalPurchaseDollarCashOutflow = 0.0; + + for (const [rowNum, inputData] of invoiceForecasts.rows.entries()) { + if (invoiceForecasts.rows[rowNum].currency === 'NGN') { + totalInvoiceNairaCashInflow += parseFloat( + inputData.nairaClosingBalance + ); + } else { + totalInvoiceDollarCashInflow += parseFloat( + inputData.dollarClosingBalance + ); + } + } + + for (const [rowNum, inputData] of saleForecasts.rows.entries()) { + if (saleForecasts.rows[rowNum].currency === 'NGN') { + totalSaleNairaCashInflow += parseFloat(inputData.nairaClosingBalance); + } else { + totalSaleDollarCashInflow += parseFloat( + inputData.dollarClosingBalance + ); + } + } + + for (const [rowNum, inputData] of billForecasts.rows.entries()) { + if (billForecasts.rows[rowNum].currency === 'NGN') { + totalBillNairaCashOutflow += parseFloat( + inputData.nairaClosingBalance + ); + } else { + totalBillDollarCashOutflow += parseFloat( + inputData.dollarClosingBalance + ); + } + } + + for (const [rowNum, inputData] of purchaseForecasts.rows.entries()) { + if (purchaseForecasts.rows[rowNum].currency === 'NGN') { + totalPurchaseNairaCashOutflow += parseFloat( + inputData.nairaClosingBalance + ); + } else { + totalPurchaseDollarCashOutflow += parseFloat( + inputData.dollarClosingBalance + ); + } + } + + let totalNairaInflow = + parseFloat(totalInvoiceNairaCashInflow) + + parseFloat(totalSaleNairaCashInflow) - + parseFloat(customerPaymentForecastNairaClosingBalance) + + let totalDollarInflow = + parseFloat(totalInvoiceDollarCashInflow) + + parseFloat(totalSaleDollarCashInflow) - + parseFloat(customerPaymentForecastDollarClosingBalance) + + let totalNairaOutflow = + parseFloat(totalBillNairaCashOutflow) + + parseFloat(totalPurchaseNairaCashOutflow) - + parseFloat(vendorPaymentForecastNairaClosingBalance) + + + let totalDollarOutflow = + parseFloat(totalBillDollarCashOutflow) + + parseFloat(totalPurchaseDollarCashOutflow) - + parseFloat(vendorPaymentForecastDollarClosingBalance) + + // nairaNetWorkingCapital + let totalNairaNetWorkingCapital = + parseFloat(initialOpeningBalance.openingBalance) + + totalNairaInflow - + totalNairaOutflow; + // dollarNetWorkingCapital + let totalDollarNetWorkingCapital = + parseFloat(initialOpeningBalance.dollarOpeningBalance) + + totalDollarInflow - + totalDollarOutflow; + + statusCode = 200; + result = { + status: true, + message: 'Report generated succesfully', + data: { + report: { + openingBalance: { + naira: initialOpeningBalance.openingBalance, + dollar: initialOpeningBalance.dollarOpeningBalance, + }, + totalCashInflow: { + naira: totalNairaInflow, + dollar: totalDollarInflow, + }, + totalCashOutflow: { + naira: totalNairaOutflow, + dollar: totalDollarOutflow, + }, + closingBalance: { + naira: totalNairaNetWorkingCapital, + dollar: totalDollarNetWorkingCapital, + }, + }, + invoices: invoices.rows, + bills: bills.rows, + sales: sales.rows, + purchases: purchases.rows, + customerPayments: customerPayments.rows, + vendorPayments: vendorPayments.rows, + }, + }; + } + } catch (e) { + console.log(e); + statusCode = e.response.status; + result = { + status: false, + message: e.response.data.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +const salesOrderHandler = async (req, reply) => { + try { + let res; + let zohoAccessToken = req.body.zohoAccessToken; + + const options = { + headers: { + 'Content-Type': ['application/json'], + Authorization: 'Bearer ' + zohoAccessToken, + }, + }; + + let url = `${config.ZOHO_BOOK_BASE_URL}/salesorders?organization_id=${config.ORGANIZATION_ID}`; + + res = await axios.get(url, options); + + if (res.data.error) + return reply.code(400).send({ + status: false, + message: 'Could not fetch sales order', + }); + + statusCode = 200; + + result = { + status: true, + message: 'Sales fetched successfully', + data: res.data, + }; + } catch (e) { + statusCode = e.response.status; + result = { + status: false, + message: e.response.data.message, + }; + } + + return reply.status(statusCode).send(result); +}; + +module.exports = { + generateReportHandler, + salesOrderHandler, +}; diff --git a/src/emailTemplates/index.html b/src/emailTemplates/index.html new file mode 100644 index 0000000..897f01d --- /dev/null +++ b/src/emailTemplates/index.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+

Hello,

+

+ Congratulations!!! You have being invited to complete the registration + process for Manifold Computers Limited Forecasting Application.

+

+ You need to click on the link below to complete your registration + process. The link will expire in the next 24 hours

+

+ Thank you,
Manifold Computers Limited

+
+
+ + + + + + + + + + + + + +
+

+ Complete registration by clicking here

+
+

+ Manifold + Computers Limited +

+ +
+

Get the + most of Manifold Computer Limited Forecasting Application by running it on + the web platfrom.

+
+
+
+ + + \ No newline at end of file diff --git a/src/emailTemplates/logo.png b/src/emailTemplates/logo.png new file mode 100644 index 0000000..722c25f Binary files /dev/null and b/src/emailTemplates/logo.png differ diff --git a/src/emailTemplates/password_reset_email_template.html b/src/emailTemplates/password_reset_email_template.html new file mode 100644 index 0000000..80ad11e --- /dev/null +++ b/src/emailTemplates/password_reset_email_template.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+

Hello {{username}}

+ + + +

+ The password for your Manifold Computers Limited Forecasting Application + (email ) has been successfully reset. +

+ +

+ Update your password by clicking on this link: Reset + Password +

+ +

+ If you did not make this change or you believe an unauthorized person + has accessed your account, + go to [https://www.manifoldcomputers.com] to + reset your password without + delay.

+ +

+ Following this, sign in to your Forecasting Application account page at + [https://www.manifoldcomputers.com] to + review + and update your security + settings. + If you need additional help, please contact Manifold Computers Limited + Support. + +

+ + +

+ Thanks, +
Manifold Computers Limited + +

+ + + +
+
+ + + + + + + + + + + + + +
+ +

+ Update your password by clicking on this link: Reset + Password +

+
+

+ Manifold + Computers Limited +

+ +
+

Get the + most of Manifold Computer Limited Forecasting Application by running it on + the web platfrom.

+
+
+
+ + + \ No newline at end of file diff --git a/src/emailTemplates/signup_email_template.html b/src/emailTemplates/signup_email_template.html new file mode 100644 index 0000000..5eabd7b --- /dev/null +++ b/src/emailTemplates/signup_email_template.html @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+

Hello {{username}}

+

+

Welcome to the Manifold Forecasting Application

+

+ Manifold Computers Limited Forecasting Application allow you to + ..... +

+ +
+
+ + + + + + + + + + + + + + +
+

+ +

Click here to get started

+ +

+

+ You can access Manifold computers limited forecasting applicaiton or on any + device by going to https://www.manifoldcomputers.com +

+
+

+ Manifold + Computers Limited +

+
+

Get the + most of Manifold Computer Limited Forecasting Application by running it on + the web platfrom.

+
+
+
+ + + \ No newline at end of file diff --git a/src/helpers/dbQuery.js b/src/helpers/dbQuery.js new file mode 100644 index 0000000..d877dce --- /dev/null +++ b/src/helpers/dbQuery.js @@ -0,0 +1,399 @@ +const { Op } = require('sequelize'); +const db = require('../models'); + +const InvoiceForecast = db.invoiceForecasts; +const PurchaseForecast = db.purchaseForecasts; +const SaleForecast = db.saleForecasts; +const BillForecast = db.billForecasts; +const ZohoRate = db.zohorates; +const VendorPayment = db.vendorPayments; +const CustomerPayment = db.customerPayments; + +const OpeningBalance = db.openingBalances; +const BankAccount = db.bankAccounts; +const accrued = db.accruedexp; +const prepaidExpense = db.prepaidexp; + + + +const Invoice = db.invoices; +const Bill = db.bills; +const Purchase = db.purchases; +const Sale = db.sales; + +const InitialBalance = db.initialBalances; + +const createInvoiceForecast = async ( + userId, + naira, + dollar, + month, + forecastNumber, + forecastPeriod, + currency +) => { + await InvoiceForecast.create({ + userId: userId, + nairaClosingBalance: naira, + dollarClosingBalance: dollar, + month: month, + forecastType: `${forecastNumber} ${forecastPeriod}`, + currency: currency, + }); +}; + +const createBillForecast = async ( + userId, + naira, + dollar, + month, + forecastNumber, + forecastPeriod, + currency +) => { + await BillForecast.create({ + userId: userId, + nairaClosingBalance: naira, + dollarClosingBalance: dollar, + month: month, + forecastType: `${forecastNumber} ${forecastPeriod}`, + currency: currency, + }); +}; + +const createPurchaseForecast = async ( + userId, + naira, + dollar, + month, + forecastNumber, + forecastPeriod, + currency +) => { + await PurchaseForecast.create({ + userId: userId, + nairaClosingBalance: naira, + dollarClosingBalance: dollar, + month: month, + forecastType: `${forecastNumber} ${forecastPeriod}`, + currency: currency, + }); +}; + +const createSaleForecast = async ( + userId, + naira, + dollar, + month, + forecastNumber, + forecastPeriod, + currency +) => { + await SaleForecast.create({ + userId: userId, + nairaClosingBalance: naira, + dollarClosingBalance: dollar, + month: month, + forecastType: `${forecastNumber} ${forecastPeriod}`, + currency: currency, + }); +}; + +const createInvoice = async ({ payload }) => { + await Invoice.create(payload); +}; + +const createBill = async ({ payload }) => { + await Bill.create(payload); +}; + +const createPurchase = async ({ payload }) => { + await Purchase.create(payload); +}; + +const createSale = async ({ payload }) => { + await Sale.create(payload); +}; + +const fetchAllInvoice = async ({ payload }) => { + return await Invoice.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllBill = async ({ payload }) => { + return await Bill.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllPurchase = async ({ payload }) => { + return await Purchase.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllSale = async ({ payload }) => { + return await Sale.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const getInitialBalance = async ({ payload }) => { + return await InitialBalance.findOne({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllInvoiceForecast = async ({ payload }) => { + return await InvoiceForecast.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllBillForecast = async ({ payload }) => { + return await BillForecast.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllPurchaseForecast = async ({ payload }) => { + return await PurchaseForecast.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllSaleForecast = async ({ payload }) => { + return await SaleForecast.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const getPurchaseForecast = async ({ payload }) => { + return await PurchaseForecast.findOne({ + where: { + userId: payload.userId, + forecastType: payload.forecastType, + currency: payload.currency, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const getSaleForecast = async ({ payload }) => { + return await SaleForecast.findOne({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + currency: payload.currency, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const createVendorPayment = async ({ payload }) => { + return await VendorPayment.create(payload); +}; + +const createCustomerPayment = async ({ payload }) => { + return await CustomerPayment.create(payload); +}; + +const getVendorPaymentByVendorId = async ({ payload }) => { + return await VendorPayment.findOne({ + where: { + userId: payload.userId, + vendorId: payload.vendorId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + currencyCode: payload.currencyCode, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const getCustomerPaymentByCustomerId = async ({ payload }) => { + return await CustomerPayment.findOne({ + where: { + userId: payload.userId, + customerId: payload.customerId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + currencyCode: payload.currencyCode, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllVendorPayments = async ({ payload }) => { + return await VendorPayment.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const fetchAllCustomerPayments = async ({ payload }) => { + return await CustomerPayment.findAndCountAll({ + where: { + userId: payload.userId, + forecastType: `${payload.forecastNumber} ${payload.forecastPeriod}`, + createdAt: { + [Op.gt]: payload.today_start, + [Op.lt]: payload.today_end, + }, + }, + }); +}; + +const createInitialBalance = async ({ startingBalance }) => { + return await InitialBalance.create(startingBalance); +}; + +const createOpeningBalance = async ({ payload }) => { + return await OpeningBalance.create(payload); +}; + +const createBankAccounts = async ({ bankAccounts }) => { + return await BankAccount.create(bankAccounts); +}; + +const getTodayDayOpeningBalance = async ({ todayOpeningBalData }) => { + console.log('today', todayOpeningBalData); + return await OpeningBalance.findOne({ + where: { + createdAt: { + [Op.gt]: todayOpeningBalData.today_start, + [Op.lt]: todayOpeningBalData.today_end, + }, + }, + }); +}; + +const getPreviousDayOpeningBalance = async ({ prevOpeningBalData }) => { + console.log('prev', prevOpeningBalData); + return await OpeningBalance.findOne({ + where: { + createdAt: { + [Op.gt]: prevOpeningBalData.yesterday_start, + [Op.lt]: prevOpeningBalData.yesterday_end, + }, + }, + }); +}; + +const createZohoRate = async ({ payload }) => { + return await ZohoRate.create(payload); +}; + +module.exports = { + createZohoRate, + createInvoiceForecast, + createInvoice, + createBillForecast, + createBill, + createPurchaseForecast, + createPurchase, + createSaleForecast, + createSale, + createVendorPayment, + createCustomerPayment, + fetchAllInvoice, + fetchAllBill, + fetchAllPurchase, + fetchAllSale, + getInitialBalance, + fetchAllInvoiceForecast, + fetchAllBillForecast, + fetchAllPurchaseForecast, + fetchAllSaleForecast, + fetchAllVendorPayments, + fetchAllCustomerPayments, + getVendorPaymentByVendorId, + getCustomerPaymentByCustomerId, + getPurchaseForecast, + getSaleForecast, + createInitialBalance, + createOpeningBalance, + createBankAccounts, + getTodayDayOpeningBalance, + getPreviousDayOpeningBalance, +}; diff --git a/src/helpers/email.js b/src/helpers/email.js new file mode 100644 index 0000000..dbd101c --- /dev/null +++ b/src/helpers/email.js @@ -0,0 +1,77 @@ +const nodemailer = require('nodemailer'); +const Handlebars = require('handlebars'); +const fs = require('fs'); +const path = require('path'); +const config = require('../../config'); + +const sendEmailTemplate = async (templateDetails) => { + const { name, templateToUse, url } = templateDetails; + let filePath = ''; + let template = ''; + + try { + //template could be any of the following: invite, passwordChanged. + if (templateToUse == 'invite') { + filePath = path.join(__dirname, '../emailTemplates/index.html'); + } else if (templateToUse == 'signup') { + filePath = path.join( + __dirname, + '../emailTemplates/signup_email_template.html' + ); + } else if (templateToUse == 'passwordReset') { + filePath = path.join( + __dirname, + '../emailTemplates/password_reset_email_template.html' + ); + } + + const source = fs.readFileSync(filePath, 'utf-8').toString(); + template = Handlebars.compile(source); + const replacements = { + username: name ?? '', + url: url ?? '', + }; + + const htmlToSend = template(replacements); + return htmlToSend; + } catch (error) { + return error; + } +}; + +async function sendEmail(to, subject, body, detail) { + try { + const transporter = nodemailer.createTransport({ + host: config.SMTP_HOST, + port: config.SMTP_PORT, + secure: false, // upgrade later with STARTTLS + tls: { + ciphers: 'SSLv3', + }, + auth: { + user: config.SMTP_USER, + pass: `${config.SMTP_PASS}`, + }, + }); + + const template = await sendEmailTemplate(detail); + + const message = { + from: `${config.EMAIL_FROM} "MANIFOLD FORECAST"`, + to: to, + subject: subject, + text: body, + html: template, + }; + + const result = await transporter.sendMail(message); + return result; + } catch (error) { + return error; + } +} + +module.exports = { + sendEmail, + sendEmailTemplate, +}; diff --git a/src/helpers/status.js b/src/helpers/status.js new file mode 100644 index 0000000..3c1bc9c --- /dev/null +++ b/src/helpers/status.js @@ -0,0 +1,46 @@ +const badRequest = { + type: 'object', + description: 'Bad Request', + properties: { + status: { type: 'boolean' }, + message: { type: 'string' }, + }, +}; + +const internalServer = { + type: 'object', + description: 'Internal Server Error', + properties: { + status: { type: 'boolean' }, + message: { type: 'string' }, + }, +}; + +const noConent = { + type: 'object', + description: 'No Content', + properties: { + status: { type: 'boolean' }, + message: { type: 'string' }, + }, +}; + +const success = { + type: 'object', + description: 'OK', + properties: { + status: { type: 'boolean' }, + message: { type: 'string' }, + data: {}, + }, +}; + +const created = { + type: 'object', + description: 'Created', + properties: { + status: { type: 'boolean' }, + message: { type: 'string' }, + }, +}; +module.exports = { badRequest, internalServer, noConent, success, created }; diff --git a/src/models/accruedExpenses.forecast.model.js b/src/models/accruedExpenses.forecast.model.js new file mode 100644 index 0000000..c03578d --- /dev/null +++ b/src/models/accruedExpenses.forecast.model.js @@ -0,0 +1,54 @@ +module.exports = (sequelize, DataTypes) => { + const AccruedExpense = sequelize.define('accruedexpenses', { + account_id: { + type: DataTypes.STRING, + allowNull: false, + + }, + tax_id: { + type: DataTypes.STRING, + allowNull: true, + + }, + payment_date: { + type: DataTypes.DATE, + allowNull: false, + }, + is_taxable: { + type: DataTypes.BOOLEAN, + defaultValue: false, + + }, + amount: { + type: DataTypes.DECIMAL(12, 2), + }, + // role is true:1 for admin and false:0 for user + reference_number: { + type: DataTypes.STRING, + }, + customer_id: { + type: DataTypes.STRING, + defaultValue: false, + }, + description: { + type: DataTypes.TEXT('long'), + }, + token: { + type: DataTypes.TEXT('long'), + }, + currency_id: { + type: DataTypes.STRING, + }, + non_billable: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + paymentConfirmation: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + }); + + return AccruedExpense; + }; + \ No newline at end of file diff --git a/src/models/bank.account.model.js b/src/models/bank.account.model.js new file mode 100644 index 0000000..b4362e8 --- /dev/null +++ b/src/models/bank.account.model.js @@ -0,0 +1,32 @@ +module.exports = (sequelize, DataTypes) => { + const BankAccount = sequelize.define('bankAccount', { + accountId: { + type: DataTypes.STRING, + }, + accountName: { + type: DataTypes.STRING, + }, + accountType: { + type: DataTypes.STRING, + }, + accountNumber: { + type: DataTypes.STRING, + }, + bankName: { + type: DataTypes.STRING, + }, + currency: { + type: DataTypes.STRING, + }, + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + overdraftBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + }); + + return BankAccount; +}; diff --git a/src/models/bill.forecast.model.js b/src/models/bill.forecast.model.js new file mode 100644 index 0000000..ae8e3ad --- /dev/null +++ b/src/models/bill.forecast.model.js @@ -0,0 +1,27 @@ +module.exports = (sequelize, DataTypes) => { + const BillForecast = sequelize.define('billForecast', { + dollarClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + nairaClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + month: { + type: DataTypes.STRING, + }, + forecastType: { + type: DataTypes.STRING, + }, // 1 day, 1 week, 1month, 2month etc + currency: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return BillForecast; +}; diff --git a/src/models/bill.model.js b/src/models/bill.model.js new file mode 100644 index 0000000..2582567 --- /dev/null +++ b/src/models/bill.model.js @@ -0,0 +1,56 @@ +module.exports = (sequelize, DataTypes) => { + const Bill = sequelize.define(' bill', { + billId: { + type: DataTypes.STRING, + }, + vendorId: { + type: DataTypes.STRING, + }, + vendorName: { + type: DataTypes.STRING, + }, + status: { + type: DataTypes.STRING, + }, + invoiceNumber: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + dueDate: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + naira: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollar: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return Bill; +}; diff --git a/src/models/customer.payment.model.js b/src/models/customer.payment.model.js new file mode 100644 index 0000000..4b39ae9 --- /dev/null +++ b/src/models/customer.payment.model.js @@ -0,0 +1,51 @@ +module.exports = (sequelize, DataTypes) => { + const CustomerPayment = sequelize.define('customerPayment', { + paymentId: { + type: DataTypes.STRING, + }, + customerId: { + type: DataTypes.STRING, + }, + customerName: { + type: DataTypes.STRING, + }, + invoiceNumbers: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + amount: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + // Amount after deducting from purchase order + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + saleForcastbalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return CustomerPayment; +}; diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..2055fc8 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,71 @@ +require('dotenv').config(); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +let sequelize; + +sequelize = new Sequelize( + `${config.MYSQL_DATABASE}`, + `${config.MYSQL_USERNAME}`, + config.MYSQL_ROOT_PASSWORD, + { + host: config.MYSQL_HOST, + port: config.MYSQL_DOCKER_PORT, + dialect: 'mysql', + operatorsAliases: 0, + timezone: '+01:00', + + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000, + }, + } +); + +const db = {}; + +db.Sequelize = Sequelize; +db.sequelize = sequelize; + +db.users = require('./user.model')(sequelize, Sequelize); +db.prepaidexp = require('./prepaideExpenses.forecast.model')(sequelize, Sequelize); +db.accruedexp = require('./accruedExpenses.forecast.model')(sequelize, Sequelize); + +db.tokens = require('./token.model')(sequelize, Sequelize); +db.rates = require('./rate.model')(sequelize, Sequelize); +db.zohorates = require('./zoho.rate.model')(sequelize, Sequelize); +db.invoices = require('./invoice.model')(sequelize, Sequelize); +db.invoiceForecasts = require('./invoice.forecast.model')(sequelize, Sequelize); +db.purchases = require('./purchase.model')(sequelize, Sequelize); +db.purchaseForecasts = require('./purchase.forecast.model')( + sequelize, + Sequelize +); +db.sales = require('./sale.model')(sequelize, Sequelize); +db.saleForecasts = require('./sale.forecast.model')(sequelize, Sequelize); +db.bills = require('./bill.model')(sequelize, Sequelize); +db.billForecasts = require('./bill.forecast.model')(sequelize, Sequelize); +db.initialBalances = require('./initial.balance.model')(sequelize, Sequelize); +db.customerPayments = require('./customer.payment.model')(sequelize, Sequelize); +db.vendorPayments = require('./vendor.payment.model')(sequelize, Sequelize); +db.openingBalances = require('./opening.balance.model')(sequelize, Sequelize); +db.bankAccounts = require('./bank.account.model')(sequelize, Sequelize); +db.overdrafts = require('./overdraft.model')(sequelize, Sequelize); + +db.tokens.belongsTo(db.users, { foreignKey: 'userId' }); +db.users.hasMany(db.tokens, { foreignKey: 'userId' }); + +sequelize + .sync({ force: false, alter: true }) + .then(async () => { + // await db.roles.create({ + // name: "Admin", + // description: "The admin", + // }); + }) + .catch((err) => { + console.log(err); + }); +module.exports = db; diff --git a/src/models/initial.balance.model.js b/src/models/initial.balance.model.js new file mode 100644 index 0000000..7860c64 --- /dev/null +++ b/src/models/initial.balance.model.js @@ -0,0 +1,29 @@ +module.exports = (sequelize, DataTypes) => { + const InitialBalance = sequelize.define('initialBalance', { + openingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + nairaOpeningBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollarOpeningBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + rate: { + type: DataTypes.DECIMAL(10, 2), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, // 1 day, 1 week, 1month, 2month etc, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return InitialBalance; +}; diff --git a/src/models/invoice.forecast.model.js b/src/models/invoice.forecast.model.js new file mode 100644 index 0000000..c89ddff --- /dev/null +++ b/src/models/invoice.forecast.model.js @@ -0,0 +1,27 @@ +module.exports = (sequelize, DataTypes) => { + const InvoiceForecast = sequelize.define('invoiceForecast', { + dollarClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + nairaClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + month: { + type: DataTypes.STRING, + }, + forecastType: { + type: DataTypes.STRING, + }, // 1 day, 1 week, 1month, 2month etc, + currency: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return InvoiceForecast; +}; diff --git a/src/models/invoice.model.js b/src/models/invoice.model.js new file mode 100644 index 0000000..6061092 --- /dev/null +++ b/src/models/invoice.model.js @@ -0,0 +1,53 @@ +module.exports = (sequelize, DataTypes) => { + const Invoice = sequelize.define('invoice', { + invoiceId: { + type: DataTypes.STRING, + }, + customerName: { + type: DataTypes.STRING, + }, + status: { + type: DataTypes.STRING, + }, + invoiceNumber: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + dueDate: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + naira: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollar: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return Invoice; +}; diff --git a/src/models/opening.balance.model.js b/src/models/opening.balance.model.js new file mode 100644 index 0000000..990ad0a --- /dev/null +++ b/src/models/opening.balance.model.js @@ -0,0 +1,14 @@ +module.exports = (sequelize, DataTypes) => { + const OpeningBalance = sequelize.define('openingBalance', { + naira: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollar: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + }); + + return OpeningBalance; +}; diff --git a/src/models/overdraft.model.js b/src/models/overdraft.model.js new file mode 100644 index 0000000..50094ea --- /dev/null +++ b/src/models/overdraft.model.js @@ -0,0 +1,28 @@ +module.exports = (sequelize, DataTypes) => { + const Overdraft = sequelize.define('overdraft', { + accountId: { + type: DataTypes.STRING, + }, + accountName: { + type: DataTypes.STRING, + }, + accountType: { + type: DataTypes.STRING, + }, + accountNumber: { + type: DataTypes.STRING, + }, + bankName: { + type: DataTypes.STRING, + }, + currency: { + type: DataTypes.STRING, + }, + amount: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + }); + + return Overdraft; +}; diff --git a/src/models/prepaideExpenses.forecast.model.js b/src/models/prepaideExpenses.forecast.model.js new file mode 100644 index 0000000..ad31b0a --- /dev/null +++ b/src/models/prepaideExpenses.forecast.model.js @@ -0,0 +1,54 @@ +module.exports = (sequelize, DataTypes) => { + const PrepaidExpense = sequelize.define('prepaidexpenses', { + account_id: { + type: DataTypes.STRING, + allowNull: false, + + }, + tax_id: { + type: DataTypes.STRING, + allowNull: true, + + }, + payment_date: { + type: DataTypes.DATE, + allowNull: false, + }, + is_taxable: { + type: DataTypes.BOOLEAN, + defaultValue: false, + + }, + amount: { + type: DataTypes.DECIMAL(12, 2), + }, + // role is true:1 for admin and false:0 for user + reference_number: { + type: DataTypes.STRING, + }, + customer_id: { + type: DataTypes.STRING, + defaultValue: false, + }, + description: { + type: DataTypes.TEXT('long'), + }, + token: { + type: DataTypes.TEXT('long'), + }, + currency_id: { + type: DataTypes.STRING, + }, + non_billable: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + paymentConfirmation: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, + }); + + return PrepaidExpense; + }; \ No newline at end of file diff --git a/src/models/purchase.forecast.model.js b/src/models/purchase.forecast.model.js new file mode 100644 index 0000000..34f8c08 --- /dev/null +++ b/src/models/purchase.forecast.model.js @@ -0,0 +1,27 @@ +module.exports = (sequelize, DataTypes) => { + const PurchaseForecast = sequelize.define('purchaseForecast', { + dollarClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + nairaClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + month: { + type: DataTypes.STRING, + }, + forecastType: { + type: DataTypes.STRING, + }, // 1 day, 1 week, 1month, 2month etc, + currency: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return PurchaseForecast; +}; diff --git a/src/models/purchase.model.js b/src/models/purchase.model.js new file mode 100644 index 0000000..48c5a77 --- /dev/null +++ b/src/models/purchase.model.js @@ -0,0 +1,61 @@ +module.exports = (sequelize, DataTypes) => { + const Purchase = sequelize.define('purchase', { + purchaseOrderId: { + type: DataTypes.STRING, + }, + vendorId: { + type: DataTypes.STRING, + }, + vendorName: { + type: DataTypes.STRING, + }, + status: { + type: DataTypes.STRING, + }, + purchaseOrderNumber: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + deliveryDate: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + total: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + naira: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollar: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + // Amount after deducting from payment made + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return Purchase; +}; diff --git a/src/models/rate.model.js b/src/models/rate.model.js new file mode 100644 index 0000000..ebb5574 --- /dev/null +++ b/src/models/rate.model.js @@ -0,0 +1,21 @@ +module.exports = (sequelize, DataTypes) => { + const Rate = sequelize.define('rate', { + old: { + type: DataTypes.DECIMAL(10, 2), + defaultValue: 0.0, + }, + latest: { + type: DataTypes.DECIMAL(10, 2), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return Rate; +}; diff --git a/src/models/sale.forecast.model.js b/src/models/sale.forecast.model.js new file mode 100644 index 0000000..24b9df4 --- /dev/null +++ b/src/models/sale.forecast.model.js @@ -0,0 +1,27 @@ +module.exports = (sequelize, DataTypes) => { + const SaleForecast = sequelize.define('saleForecast', { + dollarClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + nairaClosingBalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + month: { + type: DataTypes.STRING, + }, + forecastType: { + type: DataTypes.STRING, + }, // 1 day, 1 week, 1month, 2month etc, + currency: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return SaleForecast; +}; diff --git a/src/models/sale.model.js b/src/models/sale.model.js new file mode 100644 index 0000000..d3163d7 --- /dev/null +++ b/src/models/sale.model.js @@ -0,0 +1,61 @@ +module.exports = (sequelize, DataTypes) => { + const Sale = sequelize.define('sale', { + saleOrderId: { + type: DataTypes.STRING, + }, + customerId: { + type: DataTypes.STRING, + }, + customerName: { + type: DataTypes.STRING, + }, + status: { + type: DataTypes.STRING, + }, + salesOrderNumber: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + shipmentDate: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + total: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + naira: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + dollar: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + // Amount after deducting from payment made + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return Sale; +}; diff --git a/src/models/token.model.js b/src/models/token.model.js new file mode 100644 index 0000000..d8b9f7f --- /dev/null +++ b/src/models/token.model.js @@ -0,0 +1,17 @@ +module.exports = (sequelize, DataTypes) => { + const Token = sequelize.define('token', { + // Refresh token generated after successful authentication with zoho + zohoRefreshToken: { + type: DataTypes.TEXT('long'), + }, + zohoTokenExpiry: { + type: DataTypes.DATE, + }, + zohoTokenDate: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }); + + return Token; +}; diff --git a/src/models/user.model.js b/src/models/user.model.js new file mode 100644 index 0000000..5c4e9f7 --- /dev/null +++ b/src/models/user.model.js @@ -0,0 +1,51 @@ +module.exports = (sequelize, DataTypes) => { + const User = sequelize.define('user', { + firstName: { + type: DataTypes.STRING, + }, + lastName: { + type: DataTypes.STRING, + }, + email: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + }, + password: { + type: DataTypes.STRING, + }, + // status is true if the user has verified their email address and completed registration + status: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + // role is true:1 for admin and false:0 for user + role: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + // super admin + super: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + // Token generated when an invite is sent to a user + inviteToken: { + type: DataTypes.TEXT('long'), + }, + // Access token generated when a user logs in + token: { + type: DataTypes.TEXT('long'), + }, + inviteDate: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + isZohoAuthenticated: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + }); + + return User; +}; diff --git a/src/models/vendor.payment.model.js b/src/models/vendor.payment.model.js new file mode 100644 index 0000000..36d29ef --- /dev/null +++ b/src/models/vendor.payment.model.js @@ -0,0 +1,51 @@ +module.exports = (sequelize, DataTypes) => { + const VendorPayment = sequelize.define('vendorPayment', { + paymentId: { + type: DataTypes.STRING, + }, + vendorId: { + type: DataTypes.STRING, + }, + vendorName: { + type: DataTypes.STRING, + }, + billNumbers: { + type: DataTypes.STRING, + }, + refrenceNumber: { + type: DataTypes.STRING, + }, + date: { + type: DataTypes.STRING, + }, + currencyCode: { + type: DataTypes.STRING, + }, + amount: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + // Amount after deducting from purchase order + balance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + purchaseForcastbalance: { + type: DataTypes.DECIMAL(12, 2), + defaultValue: 0.0, + }, + exchangeRate: { + type: DataTypes.DECIMAL(10, 6), + defaultValue: 0.0, + }, + forecastType: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + return VendorPayment; +}; diff --git a/src/models/zoho.rate.model.js b/src/models/zoho.rate.model.js new file mode 100644 index 0000000..4fbafdf --- /dev/null +++ b/src/models/zoho.rate.model.js @@ -0,0 +1,10 @@ +module.exports = (sequelize, DataTypes) => { + const ZohoRate = sequelize.define('zohorate', { + rate: { + type: DataTypes.DECIMAL(10, 2), + defaultValue: 0.0, + }, + }); + + return ZohoRate; +}; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..17faab6 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,115 @@ +const authController = require('../controllers/handlers/auth.handler'); +const authSchema = require('../schemas/auth.schema'); +const verifyToken = require('../controllers/auth/verifyToken'); + +const inviteUserOpts = { + schema: authSchema.inviteUserOpts, + handler: authController.inviteUserHandler, +}; + +const registerUserOpts = { + schema: authSchema.registerUserOpts, + handler: authController.registerUserHandler, +}; + +const loginUserOpts = { + schema: authSchema.loginUserOpts, + handler: authController.loginUserHandler, +}; + +const getUserOpts = { + schema: authSchema.getUserOpts, + handler: authController.getUserHandler, +}; + +const getUsersOpts = { + schema: authSchema.getUsersOpts, + handler: authController.getUsersHandler, +}; + +const deleteUserOpts = { + handler: authController.deleteUserHandler, +}; + +const refreshTokenOpts = { + schema: authSchema.refreshTokenOpts, + handler: authController.refreshTokenHandler, +}; + +const updatePasswordOpts = { + schema: authSchema.updatePasswordOpts, + handler: authController.updatePasswordHandler, +}; + +const updateRoleOpts = { + //schema: authSchema.updatePasswordOpts, + handler: authController.updateUserRoleHandler, +}; + +const forgotPasswordOpts = { + schema: authSchema.forgotPasswordOpts, + handler: authController.forgotPasswordHandler, +}; + +const resetPasswordOpts = { + schema: authSchema.resetPasswordOpts, + handler: authController.resetPasswordHandler, +}; + +const authRoutes = async (fastify, options) => { + // invite a new user + fastify.post('/invite', inviteUserOpts); + + // register a new user + fastify.post('/register', registerUserOpts); + + // login a user + fastify.post('/login', loginUserOpts); + + // refresh token + fastify.post('/token/refresh', refreshTokenOpts); + + // forgot password + fastify.post('/password/forgot', forgotPasswordOpts); + + // reset password + fastify.post('/password/reset', resetPasswordOpts); + + fastify + .register(require('@fastify/auth')) + .after(() => privateRoutes(fastify)); +}; + +const privateRoutes = async (fastify, options) => { + // view my profile + fastify.get('/me', { + preHandler: fastify.auth([verifyToken]), + ...getUserOpts, + }); + + // view all users + fastify.get('/users', { + preHandler: fastify.auth([verifyToken]), + ...getUsersOpts, + }); + + // update user role + fastify.patch('/users/role/update', { + preHandler: fastify.auth([verifyToken]), + ...updateRoleOpts, + }); + + // update user password + fastify.post('/password/update', { + preHandler: fastify.auth([verifyToken]), + ...updatePasswordOpts, + }); + + // delete user role + fastify.delete('/users/delete/:id', { + preHandler: fastify.auth([verifyToken]), + ...deleteUserOpts, + }); +}; + +module.exports = authRoutes; diff --git a/src/routes/nobills b/src/routes/nobills new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/zoho.routes.js b/src/routes/zoho.routes.js new file mode 100644 index 0000000..f6edda0 --- /dev/null +++ b/src/routes/zoho.routes.js @@ -0,0 +1,246 @@ +const tokenController = require("../controllers/handlers/token.handler"); +const zohoController = require("../controllers/handlers/zoho.handler"); +const forecastController = require("../controllers/handlers/forecast.handler"); +const exchangeController = require("../controllers/handlers/exchange.handler"); +const verifyToken = require("../controllers/auth/verifyToken"); +const nonbillschema = require('../schemas/nonbill.schema'); +const inventorySchema = require('../schemas/inventory.schema'); +const authController = require('../controllers/handlers/auth.handler'); + + +const getZohoInventoryOpts = { + schema: inventorySchema.getZohoInventoryOpts, + handler: exchangeController.getZohoInventoryHandler, +}; + +const generateZohoTokenOpts = { + handler: tokenController.generateZohoTokenHandler, +}; + +const refreshZohoTokenOpts = { + handler: tokenController.refreshZohoTokenHandler, +}; + +const getNonbillableExpenseOpts = { + schema: nonbillschema.nonBillablesOpts, + handler: exchangeController.getNonbillableExpense, +}; +const getAccrued = { + schema: nonbillschema.accruedExpenseOpts, + handler: authController.saveAccruedPaymentHandler, + +}; +const updateAccrued = { + schema: nonbillschema.accruedExpenseOpts, + handler: authController.updateAccruedPaymentHandler, + +}; +const getPrepaid = { + schema: nonbillschema.prepaidExpenseOpt, + handler: authController.savePrepaidPaymentHandler, +}; +const updatePrepaid = { + schema: nonbillschema.prepaidExpenseOpt, + handler: authController.savePrepaidPaymentHandler, +}; +const exchangeRateOpts = { + handler: exchangeController.getExchangeRateHandler, +}; + +const exchangeRatesOpts = { + handler: exchangeController.getAllExchangeRateHandler, +}; + +const downloadExchangeRateOpts = { + handler: exchangeController.downloadExchangeRate, +}; + +const updateExchangeRateOpts = { + handler: exchangeController.updateExchangeRateHandler, +}; + +const generateReportOpts = { + handler: zohoController.generateReportHandler, +}; + +const salesOrderOpts = { + handler: zohoController.salesOrderHandler, +}; + +const resyncForecastApplicationOpts = { + handler: forecastController.resyncHandler, +}; + +const fetchBankAccountsOpts = { + handler: forecastController.bankAccountsHandler, +}; + +const createOpeningbalanceOpts = { + handler: forecastController.createOpeningBalanceHandler, +}; +const getAccruedPaymentOpts = { + handler: forecastController.ACcruedExpHandler, +}; +const getPrepaidPaymentOpts = { + handler: forecastController.PrepaidExpHandler, +}; + +const createOverdraftOpts = { + handler: forecastController.createOverdraftHandler, +}; + +const updateOverdraftOpts = { + handler: forecastController.updateOverdraftHandler, +}; + +const getOverdraftsOpts = { + handler: forecastController.getOverdraftHandler, +}; + +const getChartDataOpts = { + handler: forecastController.getChartDataHandler, +}; + +const deleteOverdraftOpts = { + handler: forecastController.deleteOverdraftHandler, +}; + +const downloadOpeningBalanceOpts = { + handler: forecastController.downloadOpeningBalance, +} + +const zohoRoutes = async (fastify, options) => { + fastify.get("/zoho/opening/balance/create", createOpeningbalanceOpts); + + fastify + .register(require("@fastify/auth")) + .after(() => privateRoutes(fastify)); +}; + +const privateRoutes = async (fastify, options) => { + // add overdraft + fastify.post("/zoho/overdraft", { + preHandler: fastify.auth([verifyToken]), + ...createOverdraftOpts, + }); + +//post inventory + fastify.post("/zoho/getinventory", { + preHandler: fastify.auth([verifyToken]), + ...getZohoInventoryOpts, + }); + + // update overdraft + fastify.put("/zoho/overdraft/:id", { + preHandler: fastify.auth([verifyToken]), + ...updateOverdraftOpts, + }); + + //download opening balance + fastify.post("/zoho/bank/opening-balance/download", { + preHandler: fastify.auth([verifyToken]), + ...downloadOpeningBalanceOpts, + }) + + // delete overdraft + fastify.delete("/zoho/overdraft/:id", { + preHandler: fastify.auth([verifyToken]), + ...deleteOverdraftOpts, + }); + + fastify.get("/zoho/overdraft", { + preHandler: fastify.auth([verifyToken]), + ...getOverdraftsOpts, + }); + + fastify.post("/zoho/chart", { + preHandler: fastify.auth([verifyToken]), + ...getChartDataOpts, + }); + + fastify.get("/zoho/bank/accounts", { + preHandler: fastify.auth([verifyToken]), + ...fetchBankAccountsOpts, + }); + fastify.get("/zoho/bank/getallprepaidexp", { + preHandler: fastify.auth([verifyToken]), + ...getPrepaidPaymentOpts, + }); + fastify.get("/zoho/bank/getallaccrued", { + preHandler: fastify.auth([verifyToken]), + ...getAccruedPaymentOpts, + }); + + fastify.post("/zoho/exchange/rate/download", { + preHandler: fastify.auth([verifyToken]), + ...downloadExchangeRateOpts, + }); + + fastify.post("/zoho/getNonbillableExpenses", { + preHandler: fastify.auth([verifyToken]), + ...getNonbillableExpenseOpts, + }); + fastify.post("/zoho/postPrepaid", { + //preHandler: fastify.auth([verifyToken]), + ...getPrepaid, + }); + fastify.put("/zoho/updatePrepaid", { + //preHandler: fastify.auth([verifyToken]), + ...updatePrepaid, + }); + fastify.post("/zoho/postAccrued", { + //preHandler: fastify.auth([verifyToken]), + ...getAccrued, + }); + fastify.put("/zoho/updateAccrued", { + //preHandler: fastify.auth([verifyToken]), + ...updateAccrued, + }); + // get exchange rate from zoho + fastify.get("/zoho/exchange/rate", { + preHandler: fastify.auth([verifyToken]), + ...exchangeRatesOpts, + }); + + fastify.get("/zoho/exchange/rate/:number/:period", { + preHandler: fastify.auth([verifyToken]), + ...exchangeRateOpts, + }); + + fastify.put("/zoho/exchange/rate/:id", { + preHandler: fastify.auth([verifyToken]), + ...updateExchangeRateOpts, + }); + + // generate report from zoho + fastify.post("/zoho/generate/report", { + preHandler: fastify.auth([verifyToken]), + ...generateReportOpts, + }); + + // get list of sales order + fastify.post("/zoho/sales/order", { + preHandler: fastify.auth([verifyToken]), + ...salesOrderOpts, + }); + + // generate a zoho token + fastify.post("/zoho/token/generate", { + preHandler: fastify.auth([verifyToken]), + ...generateZohoTokenOpts, + }); + + // refresh a zoho token + fastify.get("/zoho/token/refresh", { + preHandler: fastify.auth([verifyToken]), + ...refreshZohoTokenOpts, + }); + + // resync application + fastify.delete("/zoho/forecast/resync", { + preHandler: fastify.auth([verifyToken]), + ...resyncForecastApplicationOpts, + }); +}; + +module.exports = zohoRoutes; diff --git a/src/schemas/auth.schema.js b/src/schemas/auth.schema.js new file mode 100644 index 0000000..e1fe4a8 --- /dev/null +++ b/src/schemas/auth.schema.js @@ -0,0 +1,278 @@ +const status = require('../helpers/status'); + +const inviteUserOpts = { + tags: ['Authentication'], + description: 'Invite a new user to join the application', + body: { + required: ['email'], + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + description: 'The email of the user', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + +const registerUserOpts = { + tags: ['Authentication'], + description: 'Register a new user to join the application', + body: { + required: ['firstName', 'lastName', 'inviteToken', 'email', 'password'], + type: 'object', + properties: { + firstName: { + type: 'string', + description: 'The first name of the user', + }, + lastName: { + type: 'string', + description: 'The last name of the user', + }, + inviteToken: { + type: 'string', + description: 'The token generated when an invite is sent to a user', + }, + email: { + type: 'string', + format: 'email', + description: 'The email of the user', + }, + password: { + type: 'string', + description: 'The password of the user', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + +const loginUserOpts = { + tags: ['Authentication'], + description: 'Login a user to the application', + body: { + required: ['email', 'password'], + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + description: 'The email of the user', + }, + password: { + type: 'string', + description: 'The password of the user', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + +const refreshTokenOpts = { + tags: ['Authentication'], + description: 'A user who wants to refresh token', + body: { + required: ['refreshToken'], + type: 'object', + properties: { + refreshToken: { + type: 'string', + description: 'Token', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + +const getUserOpts = { + tags: ['Authentication'], + description: 'Get the user information', + response: { + 200: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + id: { type: 'integer' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + email: { type: 'string' }, + status: { type: 'boolean' }, + role: { type: 'boolean' }, + super: { type: 'boolean' }, + inviteToken: { type: 'string' }, + token: { type: 'string' }, + inviteDate: { type: 'string' }, + isZohoAuthenticated: { type: 'boolean' }, + }, + }, + }, + }, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, +}; + +const getUsersOpts = { + tags: ['Authentication'], + description: 'Get all user information', + response: { + 200: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'integer' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + email: { type: 'string' }, + status: { type: 'boolean' }, + role: { type: 'boolean' }, + super: { type: 'boolean' }, + inviteToken: { type: 'string' }, + token: { type: 'string' }, + inviteDate: { type: 'string' }, + isZohoAuthenticated: { type: 'boolean' }, + }, + }, + }, + }, + }, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, +}; + +const updatePasswordOpts = { + tags: ['Authentication'], + description: 'User who wants to change password', + body: { + required: ['currentPassword', 'newPassword'], + type: 'object', + properties: { + currentPassword: { + type: 'string', + description: 'Current Password', + }, + newPassword: { + type: 'string', + description: 'New Password', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + security: [ + { + apiKey: [], + }, + ], + logLevel: 'debug', +}; + +const forgotPasswordOpts = { + tags: ['Authentication'], + description: 'User who forgot password', + body: { + required: ['email'], + type: 'object', + properties: { + eamil: { + type: 'string', + description: 'Email address', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + security: [ + { + apiKey: [], + }, + ], + logLevel: 'debug', +}; + +const resetPasswordOpts = { + tags: ['Authentication'], + description: 'User who wants to reset password', + body: { + required: ['password', 'token'], + type: 'object', + properties: { + password: { + type: 'string', + description: 'Email address', + }, + token: { + type: 'string', + description: 'Token', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + security: [ + { + apiKey: [], + }, + ], + logLevel: 'debug', +}; + +module.exports = { + inviteUserOpts, + registerUserOpts, + loginUserOpts, + refreshTokenOpts, + getUserOpts, + getUsersOpts, + updatePasswordOpts, + forgotPasswordOpts, + resetPasswordOpts, +}; diff --git a/src/schemas/inventory.schema.js b/src/schemas/inventory.schema.js new file mode 100644 index 0000000..a5c3e36 --- /dev/null +++ b/src/schemas/inventory.schema.js @@ -0,0 +1,28 @@ +const status = require('../helpers/status'); + +const getZohoInventoryOpts = { + tags: ['Authentication'], + description: 'Add access token', + body: { + required: ['accessToken'], + type: 'object', + properties: { + accessToken: { + type: 'string', + description: 'the accessToken', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + + +module.exports = { + getZohoInventoryOpts , + } \ No newline at end of file diff --git a/src/schemas/nonbill.schema.js b/src/schemas/nonbill.schema.js new file mode 100644 index 0000000..3752b16 --- /dev/null +++ b/src/schemas/nonbill.schema.js @@ -0,0 +1,147 @@ +const status = require('../helpers/status'); +const nonBillablesOpts = { + tags: ['Authentication'], + description: 'Get Nonbillables', + body: { + required: [ 'accessToken'], + type: 'object', + properties: { + accessToken: { + type: 'string', + description: 'The access token is required', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', + }; + const prepaidExpenseOpt = { + tags: ['Authentication'], + description: 'Get prepaid expenses', + body: { + required: ['account_id', 'tax_id', 'payment_date', 'amount','reference_number','customer_id','description','currency_id','currency'], + type: 'object', + properties: { + id: { + type: 'integer', + description: 'The id', + }, + account_id: { + type: 'string', + description: 'The account id', + }, + tax_id: { + type: 'string', + description: 'The first name of the user', + }, + payment_date: { + type: 'string', + description: 'The payment date', + }, + amount: { + type: 'string', + description: 'The Amount ', + }, + reference_number: { + type: 'string', + description: 'The reference number of the user', + }, + customer_id: { + type: 'string', + description: 'The password of the user', + }, + description: { + type: 'string', + description: 'The password of the user', + }, + currency_id: { + type: 'string', + description: 'The password of the user', + }, + currency: { + type: 'string', + description: 'The password of the user', + }, + paymentConfirmation: { + type: 'integer', + description: 'The payment confirmation flag', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', + }; + const accruedExpenseOpts = { + tags: ['Authentication'], + description: 'Get prepaid expenses', + body: { + required: ['account_id', 'tax_id', 'payment_date', 'amount','reference_number','customer_id','description','currency_id','currency'], + type: 'object', + properties: { + id: { + type: 'integer', + description: 'The id', + }, + account_id: { + type: 'string', + description: 'The account id', + }, + tax_id: { + type: 'string', + description: 'The first name of the user', + }, + payment_date: { + type: 'string', + description: 'The payment date', + }, + amount: { + type: 'string', + description: 'The Amount ', + }, + reference_number: { + type: 'string', + description: 'The reference number of the user', + }, + customer_id: { + type: 'string', + description: 'The customerid', + }, + description: { + type: 'string', + description: 'The item description', + }, + currency_id: { + type: 'string', + description: 'The password of the user', + }, + currency: { + type: 'string', + description: 'The currency ', + }, + paymentConfirmation: { + type: 'integer', + description: 'The payment confirmation flag', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', + }; + module.exports = { + nonBillablesOpts,accruedExpenseOpts,prepaidExpenseOpt, + }; \ No newline at end of file diff --git a/src/schemas/overdraft.schema.js b/src/schemas/overdraft.schema.js new file mode 100644 index 0000000..ac7ca90 --- /dev/null +++ b/src/schemas/overdraft.schema.js @@ -0,0 +1,59 @@ +const status = require('../helpers/status'); + +const addOverdraftOpts = { + tags: ['Overdraft'], + description: 'Add an overdraft account balance', + body: { + required: [ + 'accountId', + 'accountName', + 'accountType', + 'accountNumber', + 'bankName', + 'currency', + 'amount', + ], + type: 'object', + properties: { + accountId: { + type: 'string', + description: 'Bank account id', + }, + accountName: { + type: 'string', + description: 'Bank account name', + }, + accountType: { + type: 'string', + description: 'Bank account type', + }, + accountNumber: { + type: 'string', + description: 'The account number', + }, + bankName: { + type: 'string', + description: 'Bank name', + }, + currency: { + type: 'string', + description: 'The currency of the bank account', + }, + amount: { + type: 'string', + description: 'Overdraft/loan amount for this bank account', + }, + }, + }, + response: { + 200: status.success, + 204: status.noConent, + 400: status.badRequest, + 500: status.internalServer, + }, + logLevel: 'debug', +}; + +module.exports = { + addOverdraftOpts, +};