diff --git a/.github/workflows/workflow_for_ecomm.yml b/.github/workflows/workflow_for_ecomm.yml index 64eb5fdc..a8537f60 100644 --- a/.github/workflows/workflow_for_ecomm.yml +++ b/.github/workflows/workflow_for_ecomm.yml @@ -29,7 +29,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} slug: atlp-rwanda/dynamites-ecomm-be directory: coverage/ env: @@ -57,3 +57,11 @@ jobs: FACEBOOK_APP_SECRET: ${{ secrets.FACEBOOK_APP_SECRET }} FACEBOOK_CALLBACK_URL: ${{ secrets.FACEBOOK_CALLBACK_URL }} COOKIES_KEY: ${{ secrets.COOKIES_KEY }} + XREF_ID: ${{ secrets.XREF_ID }} + APIKEY: ${{ secrets.APIKEY }} + TOKEN_URL: ${{ secrets.TOKEN_URL }} + REQUEST_TO_PAY_URL: ${{ secrets.REQUEST_TO_PAY_URL }} + TARGET_ENV: ${{ secrets.TARGET_ENV }} + VALIDATE_MOMO: ${{ secrets.VALIDATE_MOMO }} + API_KEY_URL: ${{ secrets.API_KEY_URL }} + SUBSCRIPTION_KEY: ${{ secrets.SUBSCRIPTION_KEY }} diff --git a/.gitignore b/.gitignore index b0a0ac9b..16a9e708 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules/ .env dist/ src/output.log -coverage/ - +coverage/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aa3d2039..6a519a63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,12 @@ "express": "^4.19.2", "express-validator": "^7.0.1", "handlebars": "^4.7.8", + "jest": "^29.7.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", - "mailgun-js": "^0.22.0", + "mailgun-js": "^0.6.7", "morgan": "^1.10.0", - "nock": "^13.5.4", + "node-fetch": "^2.6.7", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -51,6 +52,8 @@ "@types/jsonwebtoken": "^9.0.6", "@types/mailgun-js": "^0.22.18", "@types/morgan": "^1.9.9", + "@types/node-cron": "^3.0.11", + "@types/node-fetch": "^2.6.11", "@types/nodemailer": "^6.4.15", "@types/passport": "^1.0.16", "@types/passport-facebook": "^3.0.3", @@ -65,8 +68,9 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "node-cron": "^3.0.3", "prettier": "^3.2.5", - "ts-jest": "^29.1.4", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } @@ -1879,6 +1883,22 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.15", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", @@ -2557,17 +2577,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, - "node_modules/ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -2841,11 +2850,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3230,6 +3239,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -3379,11 +3389,6 @@ "node": ">= 0.8" } }, - "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/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3454,11 +3459,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" - }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3573,7 +3573,8 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -3625,28 +3626,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/degenerator": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", - "integrity": "sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==", - "dependencies": { - "ast-types": "0.x.x", - "escodegen": "1.x.x", - "esprima": "3.x.x" - } - }, - "node_modules/degenerator/node_modules/esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3975,19 +3954,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", - "dependencies": { - "es6-promise": "^4.0.3" - } - }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -4013,82 +3979,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -4386,6 +4276,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -4569,11 +4460,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4623,7 +4509,8 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -4660,11 +4547,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -4685,9 +4567,9 @@ } }, "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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4889,39 +4771,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", - "dependencies": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ftp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/ftp/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/ftp/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5051,64 +4900,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-uri": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", - "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", - "dependencies": { - "data-uri-to-buffer": "1", - "debug": "2", - "extend": "~3.0.2", - "file-uri-to-path": "1", - "ftp": "~0.3.10", - "readable-stream": "2" - } - }, - "node_modules/get-uri/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/get-uri/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/get-uri/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/get-uri/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/get-uri/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/get-uri/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/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5401,42 +5192,6 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dependencies": { - "agent-base": "4", - "debug": "3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -5559,9 +5314,9 @@ } }, "node_modules/inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.2.7.tgz", + "integrity": "sha512-0baJIGEJm8RVuFZ390oImj8Q0i57nZvH/gRKjLbatW2JYEnphm+IGTuHCRw5PN59nAtrrQrT83q0tnebEznz7Q==", "engines": [ "node >= 0.4.0" ] @@ -5594,11 +5349,6 @@ "node": ">= 0.4" } }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6667,11 +6417,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6879,58 +6624,77 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } }, "node_modules/mailgun-js": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.22.0.tgz", - "integrity": "sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.6.7.tgz", + "integrity": "sha512-iWRyjNFWOw3IOFN2qQQxvwdg+JwZJXN3TroR6vsAvUvP4YH0gzHWwIzdRiGeyaOBawM8gVFAI+LQPdNl9het8Q==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dependencies": { - "async": "^2.6.1", - "debug": "^4.1.0", - "form-data": "^2.3.3", - "inflection": "~1.12.0", - "is-stream": "^1.1.0", - "path-proxy": "~1.0.0", - "promisify-call": "^2.0.2", - "proxy-agent": "^3.0.3", - "tsscmp": "^1.0.6" + "debug": "~0.8.1", + "form-data": "~0.1.2", + "inflection": "~1.2.6", + "path-proxy": "~1.0", + "q": "~1.0.1", + "scmp": "~0.0.3" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.8.0" } }, "node_modules/mailgun-js/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==" + }, + "node_modules/mailgun-js/node_modules/combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==", "dependencies": { - "lodash": "^4.17.14" + "delayed-stream": "0.0.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mailgun-js/node_modules/debug": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", + "integrity": "sha512-HlXEJm99YsRjLJ8xmuz0Lq8YUwrv7hAJkTEr6/Em3sUlSUNl0UdFA+1SrY4fnykeq1FVkUEUtwRGHs9VvlYbGA==", + "engines": { + "node": "*" + } + }, + "node_modules/mailgun-js/node_modules/delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha512-v+7uBd1pqe5YtgPacIIbZ8HuHeLFVNe4mUEyFDXL6KiqzEykjbw+5mXZXpGFgNVasdL4jWKgaKIXrEHiynN1LA==", + "engines": { + "node": ">=0.4.0" } }, "node_modules/mailgun-js/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "integrity": "sha512-x8eE+nzFtAMA0YYlSxf/Qhq6vP1f8wSoZ7Aw1GuctBcmudCNuTUmmx45TfEplyb6cjsZO/jvh6+1VpZn24ez+w==", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "async": "~0.9.0", + "combined-stream": "~0.0.4", + "mime": "~1.2.11" }, "engines": { - "node": ">= 0.12" + "node": ">= 0.8" } }, - "node_modules/mailgun-js/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/mailgun-js/node_modules/mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha512-Ysa2F/nqTNGHhhm9MV8ure4+Hc+Y8AWiqUdHxsO7xu8zc92ND9f3kpALHjaP026Ft17UfxrMt95c50PLUeynBw==" }, "node_modules/make-dir": { "version": "3.1.0", @@ -7207,36 +6971,36 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, - "node_modules/netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==", - "engines": { - "node": ">= 0.4.0" - } + "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/nock": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", - "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dev": true, "dependencies": { - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "propagate": "^2.0.0" + "uuid": "8.3.2" }, "engines": { - "node": ">= 10.13" + "node": ">=6.0.0" } }, - "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-cron/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==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } }, "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==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -7613,64 +7377,6 @@ "node": ">=6" } }, - "node_modules/pac-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", - "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", - "dependencies": { - "agent-base": "^4.2.0", - "debug": "^4.1.1", - "get-uri": "^2.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "pac-resolver": "^3.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "^4.0.1" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent/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/pac-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", - "dependencies": { - "co": "^4.6.0", - "degenerator": "^1.0.4", - "ip": "^1.1.5", - "netmask": "^1.0.6", - "thunkify": "^2.1.2" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8201,22 +7907,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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/promisify-call": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", - "integrity": "sha512-ZX68J1+1Pe0I8NC0P6Ji3fDDcJceVfpoygfDLgdb1fp5vW9IRlwSpDaxe1T5HgwchyHV2DsL/pWzWikUiWEbLQ==", - "dependencies": { - "with-callback": "^1.0.2" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8230,14 +7920,6 @@ "node": ">= 6" } }, - "node_modules/propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "engines": { - "node": ">= 8" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8250,55 +7932,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", - "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", - "dependencies": { - "agent-base": "^4.2.0", - "debug": "4", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^3.0.1", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent/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/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -8343,6 +7976,16 @@ } ] }, + "node_modules/q": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.0.1.tgz", + "integrity": "sha512-18MnBaCeBX9sLRUdtxz/6onlb7wLzFxCylklyO8n27y5JxJYaGLPu4ccyc5zih58SpEzY8QmfwaWqguqXU6Y+A==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -8649,6 +8292,12 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/scmp": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-0.0.3.tgz", + "integrity": "sha512-ya4sPuUOfcrJnfC+OUqTFgFVBEMOXMS1Xopn0wwIhxKwD4eveTwJoIUN9u1QHJ47nL29/m545dV8KqI92MlHPw==", + "deprecated": "scmp v2 uses improved core crypto comparison since Node v6.6.0" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -8854,56 +8503,6 @@ "node": ">=8" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "dependencies": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - }, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "dependencies": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/socks/node_modules/ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9386,11 +8985,6 @@ "node": ">=0.2.6" } }, - "node_modules/thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10205,18 +9799,11 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/with-callback": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz", - "integrity": "sha512-zaUhn7OWgikdqWlPYpZ4rTX/6IAV0czMVyd+C6QLVrif2tATF28CYUnHBmHs2a5EaZo7bB1+plBUPHto+HW8uA==", - "engines": { - "node": ">=4" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10277,14 +9864,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", - "engines": { - "node": "*" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -10304,7 +9883,8 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yaml": { "version": "2.0.0-1", diff --git a/package.json b/package.json index 27a40937..fdb7ec4d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "format": "prettier --write .", "test": "cross-env NODE_ENV=test jest --runInBand --no-cache --detectOpenHandles", "test:ci": "cross-env NODE_ENV=test jest --runInBand --coverage --detectOpenHandles" - }, + }, "repository": { "type": "git", "url": "git+https://github.com/atlp-rwanda/dynamites-ecomm-be.git" @@ -40,8 +40,9 @@ "jest": "^29.7.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", - "mailgun-js": "^0.22.0", + "mailgun-js": "^0.6.7", "morgan": "^1.10.0", + "node-fetch": "^2.6.7", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -68,15 +69,15 @@ "coveragePathIgnorePatterns": [ "/node_modules/", "/src/emails/", - "/src/utilis/" - + "/src/utilis/", + "/src/Notification.vendor/" ], "testPathIgnorePatterns": [ "/node_modules/", "/src/emails/", "/src/middlewares/", - "/src/utilis/" - + "/src/utilis/", + "/src/Notification.vendor/" ] }, "devDependencies": { @@ -89,6 +90,8 @@ "@types/jsonwebtoken": "^9.0.6", "@types/mailgun-js": "^0.22.18", "@types/morgan": "^1.9.9", + "@types/node-cron": "^3.0.11", + "@types/node-fetch": "^2.6.11", "@types/nodemailer": "^6.4.15", "@types/passport": "^1.0.16", "@types/passport-facebook": "^3.0.3", @@ -103,6 +106,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "node-cron": "^3.0.3", "prettier": "^3.2.5", "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", diff --git a/src/Notification.vendor/EmailSendor.ts b/src/Notification.vendor/EmailSendor.ts new file mode 100644 index 00000000..2e39674b --- /dev/null +++ b/src/Notification.vendor/EmailSendor.ts @@ -0,0 +1,35 @@ +import nodemailer from 'nodemailer'; +import dotenv from 'dotenv'; + +dotenv.config(); + +async function sendEmail(vendorEmail: string, message_title: string, messageContent: string) { + try { + const transporter = nodemailer.createTransport({ + service: 'gmail', + host: 'smtp.gmail.com', + port: 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const mailOptions = { + from: `"The E-commerce Team" <${process.env.EMAIL_USER}>`, + to: vendorEmail, + subject: 'Notification from Your Company', + text: messageContent, + html: `

${messageContent.replace(/\n/g, '
')}

`, + }; + + await transporter.sendMail(mailOptions); + + } catch (error) { + throw error + } +} + +export default sendEmail; + diff --git a/src/Notification.vendor/event.services.ts b/src/Notification.vendor/event.services.ts new file mode 100644 index 00000000..12b46b83 --- /dev/null +++ b/src/Notification.vendor/event.services.ts @@ -0,0 +1,388 @@ +import { EventEmitter } from 'events'; +import Notification_box from '../database/models/inbox_notification'; +import UserModel from '../database/models/userModel'; +import Product from '../database/models/productEntity'; +import { Order } from '../database/models/orderEntity'; +import dbConnection from '../database'; +import sendEmailfunc from './EmailSendor'; +import { + added_to_cart_message, + removed_to_cart_message, + pressorder_message, + order_status_changed, + new_product_created, + updated_Product, + product_deleted, + order_canceled} from './message.Templete'; + +export const eventEmitter = new EventEmitter(); + + +interface product { + id: number; + name: string; + image: string; + gallery: string[]; + shortDesc: string; + longDesc: string; + quantity: number; + regularPrice: number; + salesPrice: number; + tags: string[]; + type: string; + isAvailable: boolean; + averageRating: number; + createdAt: Date; + updatedAt: Date; + vendor: UserModel; +} + +interface order { + id: number; + user: UserModel | null; + totalAmount: number; + status: string; + deliveryInfo: string | null; + trackingNumber: string; + createdAt: Date; + updatedAt: Date; + orderDetails: OrderDetails[]; + paid: boolean | null; +} + +interface OrderDetails { + id: number; + order: Order; + product: Product; + quantity: number; + price: number; +} + +const productRepository = dbConnection.getRepository(Product); +const userRepository = dbConnection.getRepository(UserModel); +const NotificationRepository = dbConnection.getRepository(Notification_box); +const orderRepository = dbConnection.getRepository(Order); + +eventEmitter.on('addToCart', async (product_id, userId) => { + try { + if (process.env.NODE_ENV == 'test'){ + return + } + const product = await productRepository.findOne({ + where: { id: product_id }, + select: { vendor: { firstName: true, lastName: true, picture: true, id: true, email: true } }, + relations: ['vendor'], + }); + + if (!product) { + return; + } + + else if (!product.vendor || !product.vendor.email) + { + return + } + + const User = await userRepository.findOne({ + where: { id: userId } + }); + + if (!User) { + return; + } + + const new_notification = new Notification_box(); + new_notification.product_id = product.id; + new_notification.vendor_id = product.vendor.id; + new_notification.vendor_email = product.vendor.email; + new_notification.message_title = 'your product is add to buyer cart'; + new_notification.message_content = added_to_cart_message(product, User); + await NotificationRepository.save(new_notification); + await sendEmailfunc(product.vendor.email, new_notification.message_title, new_notification.message_content) + + } + catch (error) { + throw error + + } +}); + +eventEmitter.on('removeItem', async (removeItem) => { + try { + if (process.env.NODE_ENV == 'test'){ + return + } + const product = await productRepository.findOne({ + where: { id: removeItem.product.id }, + select: { vendor: { firstName: true, lastName: true, picture: true, id: true, email: true } }, + relations: ['vendor'], + }); + + if (!product) { + return; + } + + else if (!product.vendor || !product.vendor.email) + { + return + } + const user = await userRepository.findOne({ + where: { id: removeItem.user.id } + }); + + if (!user) { + return; + } + + const new_notification = new Notification_box(); + new_notification.product_id = product.id; + new_notification.vendor_id = product.vendor.id; + new_notification.vendor_email = product.vendor.email; + new_notification.message_title = 'your product is removed to buyer cart'; + new_notification.message_content = removed_to_cart_message(product, user); + + await NotificationRepository.save(new_notification); + await sendEmailfunc(product.vendor.email, new_notification.message_title, new_notification.message_content) + + } catch (error) { + throw error + } +}); + +eventEmitter.on('pressorder', async (order:order) => { + try { + if (process.env.NODE_ENV == 'test'){ + return + } + const orderDetail = order.orderDetails + for(let i=0; i { + try { + if (process.env.NODE_ENV == 'test'){ + return + } + const order = await orderRepository.findOne({ + where: { + id: orderId, + }, + relations:['orderDetails','orderDetails.product'] + }); + if(order == null) + { + return + } + const orderDetail = order.orderDetails + + for(let i=0; i{ + try{ + if (process.env.NODE_ENV == 'test'){ + return + } + if(!product) + { + return + } + + else if (!product.vendor || !product.vendor.email) { + return + } + + const new_notification = new Notification_box(); + new_notification.product_id =product.id ; + new_notification.vendor_id = product.vendor.id + new_notification.vendor_email = product.vendor.email; + new_notification.message_title = 'Your Product was created sucessfull'; + new_notification.message_content = new_product_created(product); + + await NotificationRepository.save(new_notification); + await sendEmailfunc(product.vendor.email, new_notification.message_title, new_notification.message_content) + } + catch(error) + { + throw error + } + +}) + +eventEmitter.on('product_updated', async(product:product)=>{ + + try{ + if (process.env.NODE_ENV == 'test'){ + return + } + if(!product) + { + return + } + else if (!product.vendor || !product.vendor.email) + { + return + } + const new_notification = new Notification_box(); + new_notification.product_id =product.id ; + new_notification.vendor_id = product.vendor.id + new_notification.vendor_email = product.vendor.email; + new_notification.message_title = 'Your product was Updated succesfull'; + new_notification.message_content = updated_Product(product); + + await NotificationRepository.save(new_notification); + await sendEmailfunc(product.vendor.email, new_notification.message_title, new_notification.message_content) + } + catch(error) + { + throw error + } +}) + + +eventEmitter.on('product_deleted', async(product_id:number)=>{ + try{ + if (process.env.NODE_ENV == 'test'){ + return + } + const product= await productRepository.findOne({ + where:{id: product_id}, + relations:['vendor'] + }) + if(!product || !product.vendor || !product.vendor.email) + { + return + } + + else if (!product.vendor || !product.vendor.email) + { + return + } + const new_notification = new Notification_box(); + new_notification.product_id =product.id ; + new_notification.vendor_id = product.vendor.id + new_notification.vendor_email = product.vendor.email; + new_notification.message_title = 'Your product was deleted succesfull'; + new_notification.message_content = product_deleted(product); + + await NotificationRepository.save(new_notification); + await sendEmailfunc(product.vendor.email, new_notification.message_title, new_notification.message_content) + } + catch(error) + { + throw error + } +}) + + +eventEmitter.on('order_canceled', async (orderId) => { + try { + if (process.env.NODE_ENV == 'test'){ + return + } + const order = await orderRepository.findOne({ + where: { id: orderId }, + relations: ['orderDetails','orderDetails.product', 'orderDetails.product.vendor'], + }); + if(!order) + { + return + } + const orderDetail = order.orderDetails + + for(let i=0; i{ + + return ` + Dear ${product.vendor.firstName} ${product.vendor.lastName}, + + We are pleased to inform you that a new product has been added to a cart. + + Product Details: + - Product Name: ${product.name} + - Product ID: ${product.id} + - Added By: ${user.firstName} ${user.lastName} email: ${user.email} + - Added At: ${new Date().toLocaleString()} + + Please prepare for a potential order. + + Best regards, + The E-commerce Team + + ` +} + + + +export const removed_to_cart_message=( + product:Product, + user:UserModel +):string=>{ + + return ` + Dear ${product.vendor.firstName} ${product.vendor.lastName} + + We would like to inform you that a product has been removed from a cart. + + Product Details: + - Product Name: ${product.name} + - Product ID: ${product.id} + - Removed By: ${user.firstName} ${user.lastName} email:${user.email} + - Removed At: ${new Date().toLocaleString()} + + If you have any questions or need further information, please do not hesitate to contact us. + + Best regards, + + The E-commerce Team + ` +} + + +export const pressorder_message = ( + product:Product, + order:order +): string =>{ + + + return ` + Dear ${product.vendor.firstName} ${product.vendor.lastName}, + + We are pleased to inform you that a new order has been placed. + + Order Details: + - Order ID: ${order.id} + - order Tracked Number: ${order.trackingNumber} + - Customer Name: ${order.user?.firstName} ${order.user?.lastName} email:${order.user?.email} + - Order Date: ${order.createdAt} + - Product: ${product.name} with id ${product.id} + + Please prepare the order for shipping as soon as possible. + + Thank you for your prompt attention to this new order. + + Best regards, + + The E-commerce Team + ` +} + +export const order_status_changed = ( + product:Product, + order:order +)=>{ + + return ` + Dear ${product.vendor.firstName} ${product.vendor.lastName}, + + We hope this message finds you well. + + We would like to inform you that the status of Order ${order.id} has been updated to "${order.status}". Please find the details of the order below: + + - Order ID: ${order.id} + - Order Tracking Number: ${order.trackingNumber} + - Customer Name: ${order.user?.firstName} ${order.user?.lastName} email:${order.user?.email} + - Order Date: ${order.createdAt} + - Current Status: ${order.status} + + + If you have any questions or require further information, please do not hesitate to contact us. + + Thank you for your continued partnership. + + Best regards, + + The E-commerce Team + + + --- + + **Note:** This is an automated message. Please do not reply directly to this email. + + ` +} + +export const order_canceled = ( + product:Product, + order:order +)=>{ + return ` + + Dear ${product.vendor.firstName} ${product.vendor.lastName}, + + We regret to inform you that the following order has been canceled: + + Order Details: + - Order ID: ${order.id} + - Product Name: ${product.name} + - Product ID: ${product.id} + - Order Date: ${new Date(order.createdAt).toLocaleDateString()} + - Customer: ${order.user ? `${order.user.firstName} ${order.user.lastName}` : 'Guest'} + - Total Amount: $${order.totalAmount} + + If you have any questions or need further assistance, please do not hesitate to contact our support team. + + Best regards, + The E-commerce Team + + ` +} + + + + +export const new_product_created = ( +product:Product +)=>{ + return ` + + Hello ${product.vendor.firstName} ${product.vendor.lastName}, + + We are excited to inform you that your new product has been successfully created and listed on our platform! + + Product Details: + - Name: ${product.name} + - Short Description: ${product.shortDesc} + - Price: $${product.regularPrice} + - Availability: ${product.isAvailable ? 'In Stock' : 'Out of Stock'} + + Thank you for being a valued vendor. We wish you great success with your new product! + + Best regards, + The E-commerce Team + ` +} + +export const updated_Product = ( + product:Product +)=>{ + return ` + + Hello ${product.vendor.firstName} ${product.vendor.lastName}, + + We are pleased to inform you that your product has been successfully updated on our platform! + + Updated Product Details: + - Name: ${product.name} + - Short Description:${product.shortDesc} + - Price: $${product.regularPrice} + - Availability: ${product.isAvailable ? 'In Stock' : 'Out of Stock'} + + Thank you for continuously enhancing your product offerings. We wish you continued success! + + Best regards, + The E-commerce Team + ` +} + +export const product_deleted=( + product:Product +)=>{ + return ` + + Hello ${product.vendor.firstName} ${product.vendor.id}, + + We regret to inform you that your product has been removed from our platform. + + Deleted Product Details: + - Name: ${product.name} + - Short Description:${product.shortDesc} + - Price: $${product.regularPrice} + + If you have any questions or believe this was a mistake, please contact our support team. + + Best regards, + The E-commerce Team + ` +} + +export const product_not_availble = ( + product:Product +)=>{ + return ` + Dear ${product.vendor.firstName}, + + We regret to inform you that your product "${product.name}" is currently unavailable on our platform. + + Product Details: + - Name: ${product.name} + - Short Description: ${product.shortDesc} + - Price: $${product.regularPrice} + + If you have any questions or concerns, please feel free to contact our support team. + + Best regards, + The E-commerce Team +` +} + +export const expiried_order = ( + product:Product, + order:order +)=>{ + return ` + Hello ${product.vendor.firstName} ${product.vendor.lastName}, + + We regret to inform you that the following order has been cancelled as it was not processed within the stipulated time frame of 5 days: + + Order Details: + - Order ID: ${order.id} + - Total Amount: $${order.totalAmount} + - Order Status: ${order.status} + - Ordered At: ${order.createdAt.toLocaleString()} + + Product Details: + ${order.orderDetails.map(detail => ` + - Product Name: ${detail.product.name} + - Product ID: ${detail.product.id} + - Quantity: ${detail.quantity} + - Price: $${detail.price}`).join('\n')} + + If you have any questions or concerns, please contact our support team. + + Best regards, + The E-commerce Team + ` +} \ No newline at end of file diff --git a/src/Notification.vendor/node.cron.services.ts b/src/Notification.vendor/node.cron.services.ts new file mode 100644 index 00000000..3cf22d98 --- /dev/null +++ b/src/Notification.vendor/node.cron.services.ts @@ -0,0 +1,93 @@ +import { EventEmitter } from 'events'; +import Notification_box from '../database/models/inbox_notification'; +import Product from '../database/models/productEntity'; +import { Order } from '../database/models/orderEntity'; +import dbConnection from '../database'; +import sendEmailfunc from './EmailSendor'; +import cron from 'node-cron'; +import { + product_not_availble, + expiried_order} from './message.Templete'; + +export const eventEmitter = new EventEmitter(); + +const productRepository = dbConnection.getRepository(Product); +const NotificationRepository = dbConnection.getRepository(Notification_box); +const orderRepository = dbConnection.getRepository(Order); + + +const dailyTasks = cron.schedule('0 0 * * *', async () => { + + const orders = await orderRepository.find(); + for (const order of orders) { + const createdAt = new Date(order.createdAt); + const now = new Date(); + const timeDiff = now.getTime() - createdAt.getTime(); + const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24)); + + if (diffDays >= 0.1 && order.status !== 'Canceled') { + order.status = 'Canceled'; + await orderRepository.save(order); + + // ************************************************************************* + + const orderDetail = order.orderDetails + + for(let i=0; i { + (fetch as jest.MockedFunction).mockResolvedValueOnce( + new Response(JSON.stringify(body), { status }) + ); +}; + +// Mocking the response for requestToPayStatus function +const mockRequestToPayStatusResponse = (status: number, body: Ibody) => { + (fetch as jest.MockedFunction).mockResolvedValueOnce( + new Response(JSON.stringify(body), { status }) + ); +}; + +describe('Buyer Controller Tests', () => { + let token: string; + let order: Order; + let requestId: string; + + beforeAll(async () => { + await dbConnection.initialize(); + await dbConnection.synchronize(true); + token = await getBuyerToken(); + + // Create a mock order in the database + const orderRepository = dbConnection.getRepository(Order); + order = orderRepository.create({ + totalAmount: 100, + status: 'Pending', + trackingNumber: '123456', + paid: false, + }); + await orderRepository.save(order); + }); + + afterAll(async () => { + await dbConnection.close(); + }); + + describe('MomohandlePayment', () => { + it('should handle mobile money payment successfully', async () => { + mockRequestToPayResponse(200, { apiKey: 'fake-api-key' }); + mockRequestToPayResponse(200, { access_token: 'fake-access-token' }); + mockRequestToPayResponse(200, { result: true }); + mockRequestToPayResponse(202, {}); + const response = await request(app) + .post('/api/v1/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ orderId: order.id, momoNumber: '123456789' }); + + requestId = response.body.requestId; + + expect(response.status).toBe(202); + expect(response.body.message).toBe('Transaction Accepted'); + expect(response.body.requestId).toBeDefined(); + }); + + it('should return 400 if MoMo number is invalid', async () => { + mockRequestToPayResponse(200, { apiKey: 'fake-api-key' }); + mockRequestToPayResponse(200, { access_token: 'fake-access-token' }); + mockRequestToPayResponse(200, { result: false }); + + const response = await request(app) + .post('/api/v1/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ orderId: order.id, momoNumber: 'invalid-number' }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Your Momo Number does not Exist'); + }); + + it('should return 404 if order not found', async () => { + const response = await request(app) + .post('/api/v1/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ orderId: 999, momoNumber: '123456789' }); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Order not found'); + }); + + it('should return 400 if order already paid', async () => { + const orderRepository = dbConnection.getRepository(Order); + order.paid = true; + await orderRepository.save(order); + + const response = await request(app) + .post('/api/v1/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ orderId: order.id, momoNumber: '123456789' }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Order has already been paid'); + }); + }); + + describe('checkPaymentStatus', () => { + it('should update order status successfully', async () => { + mockRequestToPayResponse(200, { apiKey: 'fake-api-key' }); + mockRequestToPayResponse(200, { access_token: 'fake-access-token' }); + mockRequestToPayStatusResponse(200, { status: 'SUCCESSFUL' }); + + const response = await request(app) + .post(`/api/v1/buyer/getPaymentStatus/${order.id}`) + .set('Authorization', `Bearer ${token}`) + .send({ requestId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.message).toBe('Transaction Done Successfully'); + }); + + it('should return 404 if order not found', async () => { + const response = await request(app) + .post('/api/v1/buyer/getPaymentStatus/99') + .set('Authorization', `Bearer ${token}`) + .send({ requestId: 'valid-request-id' }); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Order not found'); + }); + + it('should return 400 if transaction failed', async () => { + mockRequestToPayResponse(400, { apiKey: 'fake-api-key' }); + mockRequestToPayResponse(400, { access_token: 'fake-access-token' }); + mockRequestToPayStatusResponse(400, { + status: 'FAILED', + }); + + const response = await request(app) + .post(`/api/v1/buyer/getPaymentStatus/${order.id}`) + .set('Authorization', `Bearer ${token}`) + .send({ requestId }); + + expect(response.status).toBe(200); + }); + }); +}); diff --git a/src/__test__/notification.vendor.test.ts b/src/__test__/notification.vendor.test.ts new file mode 100644 index 00000000..bb0c838f --- /dev/null +++ b/src/__test__/notification.vendor.test.ts @@ -0,0 +1,71 @@ +import request from 'supertest'; +import app from '../app'; +import { getVendorToken, afterAllHook, beforeAllHook } from './testSetup'; +import Notification_box from '../database/models/inbox_notification'; +import dbConnection from '../database'; + +const notificationRepository = dbConnection.getRepository(Notification_box); + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Notification Controller Tests', () => { + let token: string; + let vendor_id: number; + let notification_id: number + + beforeAll(async () => { + token = await getVendorToken(); + + // make notification for test + // ------------------------------------------ + const notification = new Notification_box(); + notification.product_id= 20; + notification.vendor_email= 'ericniyibizi1998@gmail.com'; + notification.message_title = 'Test Notification'; + notification.message_content = 'This is a test notification'; + notification.vendor_id = 1; + const savedNotification = await notificationRepository.save(notification); + // -------------------------------------------------- + vendor_id = savedNotification.vendor_id + notification_id = savedNotification.notification_id + }); + + it('should retrieve all notifications', async () => { + const response = await request(app) + .get('/api/v1/notification/vendor') + + expect(response.statusCode).toEqual(200); + expect(response.body.msg).toEqual('Notification retrieved successfully'); + expect(Array.isArray(response.body.notification)).toBeTruthy(); + + }); + + it('should retrieve notifications by vendor ID', async () => { + const response = await request(app) + .get(`/api/v1/notification/vendor/${vendor_id}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.msg).toEqual('Notifications retrieved successfully'); + expect(Array.isArray(response.body.notification)).toBeTruthy(); + }); + + it('should delete notification by notification ID', async () => { + const response = await request(app) + .delete(`/api/v1/notification/vendor/${notification_id}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + }); + + it('should delete all notifications', async () => { + const response = await request(app) + .delete('/api/v1/notification/vendor') + + expect(response.statusCode).toEqual(200); + expect(response.body.msg).toEqual('All notifications deleted successfully'); + }); + + +}); diff --git a/src/__test__/payment.test.ts b/src/__test__/payment.test.ts index 5339156d..59ac22fc 100644 --- a/src/__test__/payment.test.ts +++ b/src/__test__/payment.test.ts @@ -48,9 +48,6 @@ describe('handlePayment', () => { .set('Authorization', `Bearer ${token}`) .send({ token: 'fake-token', orderId: order.id }); - console.log('Response status:', response.status); // Debugging line - console.log('Response body:', response.body); // Debugging line - expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.paid).toBe(true); diff --git a/src/controller/buyerController.ts b/src/controller/buyerController.ts index 632eb4d6..4a4633dd 100644 --- a/src/controller/buyerController.ts +++ b/src/controller/buyerController.ts @@ -4,6 +4,7 @@ import Product from '../database/models/productEntity'; import errorHandler from '../middlewares/errorHandler'; import Stripe from 'stripe'; import { Order } from '../database/models/orderEntity'; +import crypto from 'crypto'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { apiVersion: '2024-04-10', @@ -63,13 +64,239 @@ export const handlePayment = errorHandler( return res.status(200).json({ success: true, paid: true, charge }); } else { + return res.status(400).json({ + success: false, + paid: false, + message: `Charge status: ${charge.status}`, + }); + } + } +); + +type Idata = { + access_token: string; + token_type: string; + expires_in: string; +}; + +type IStatus = { + amount: string; + currency: string; + externalId: string; + payer: object; + payerMessage: string; + payeeNote: string; + status: string; + reason: object; +}; + +type Ivalidate = { + result: boolean; +}; + +const XRefId = process.env.XREF_ID as string; +const tokenUrl = process.env.TOKEN_URL as string; +const subscriptionKey = process.env.SUBSCRIPTION_KEY as string; +const requesttoPayUrl = process.env.REQUEST_TO_PAY_URL as string; +const targetEnv = process.env.TARGET_ENV as string; +const apiKeyUrl = process.env.API_KEY_URL as string; + +export const GenerateApiKey = async (): Promise => { + try { + const response = await fetch(`${apiKeyUrl}/${XRefId}/apikey`, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = (await response.json()) as { apiKey: string }; + return data.apiKey; + } else { + return null; + } + } catch (error) { + return null; + } +}; + +export const purchaseAccessToken = async (): Promise => { + const apiKey = await GenerateApiKey(); + if (!apiKey) { + return null; + } + const basicAuth = btoa(`${XRefId}:${apiKey}`); + try { + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + Authorization: `Basic ${basicAuth}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = (await response.json()) as Idata; + return data.access_token; + } else { + return null; + } + } catch (error) { + return null; + } +}; + +export async function requestToPay( + token: string, + xrefid: string, + externalId: string, + currency: string, + amount: string, + number: string, + payerMsg: string, + payeeNote: string +) { + const response = await fetch(requesttoPayUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${token}`, + 'X-Target-Environment': targetEnv, + 'X-Reference-Id': xrefid, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + amount: amount, + currency: currency, + externalId: externalId, + payer: { + partyIdType: 'MSISDN', + partyId: number, + }, + payerMessage: payerMsg, + payeeNote: payeeNote, + }), + }); + return response; +} + +export async function requestToPayStatus(id: string, token: string) { + const response = await fetch(`${requesttoPayUrl}/${id}`, { + method: 'GET', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${token}`, + 'X-Target-Environment': targetEnv, + }, + }); + const data = (await response.json()) as IStatus; + return data; +} + +export const validateMomo = async (token: string, momoaccount: string) => { + const validateURL = `${process.env.VALIDATE_MOMO}/${momoaccount}/active`; + try { + const resp = await fetch(validateURL, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Target-Environment': targetEnv, + }, + }); + + const response = (await resp.json()) as Ivalidate; + return response.result; + } catch (error) { + return null; + } +}; + +export const MomohandlePayment = errorHandler( + async (req: Request, res: Response) => { + const token = (await purchaseAccessToken()) as string; + const { orderId, momoNumber } = req.body; + const isValid = await validateMomo(token, momoNumber); + + if (!isValid) { return res .status(400) - .json({ - success: false, - paid: false, - message: `Charge status: ${charge.status}`, - }); + .json({ message: 'Your Momo Number does not Exist' }); } + const order = await orderRepository.findOne({ where: { id: orderId } }); + + if (!order) { + return res + .status(404) + .json({ success: false, message: 'Order not found' }); + } + + if (order.paid) { + return res + .status(400) + .json({ success: false, message: 'Order has already been paid' }); + } + + const requestId = crypto.randomUUID(); + const externalId = crypto.randomUUID(); + + const response = await requestToPay( + token, + requestId, + externalId, + 'EUR', + order.totalAmount.toString(), + momoNumber, + `paid by ${momoNumber}`, + `paid to ${momoNumber}` + ); + + if (response.ok) { + return res + .status(202) + .json({ message: 'Transaction Accepted', requestId }); + } + return res.status(400).json({ message: 'Transaction Fail' }); + } +); +export const checkPaymentStatus = errorHandler( + async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const { requestId } = req.body; + const order = await orderRepository.findOne({ where: { id: id } }); + + if (!order) { + return res + .status(404) + .json({ success: false, message: 'Order not found' }); + } + + const token = (await purchaseAccessToken()) as string; + + const data = await requestToPayStatus(requestId, token); + + if (data === null) { + return res + .status(500) + .json({ success: false, message: 'Error occurred during validation' }); + } + + if (data.status === 'SUCCESSFUL') { + order.paid = true; + await orderRepository.save(order); + return res + .status(200) + .json({ success: true, message: 'Transaction Done Successfully' }); + } + return res.status(400).json({ + success: false, + message: 'Transaction failed', + reason: data.reason, + }); } ); diff --git a/src/controller/buyerWishlistController.ts b/src/controller/buyerWishlistController.ts index 49b7389e..e38bfbd1 100644 --- a/src/controller/buyerWishlistController.ts +++ b/src/controller/buyerWishlistController.ts @@ -26,13 +26,14 @@ export const AddItemInWishList = [ const wishListTime = time ? new Date(time) : new Date(); const user = await userRepository.findOne({ where: { id: userId } }); + const product = await productRepository.findOne({ + where: { id: productId }, + }); + if (!user) { return res.status(404).json({ message: 'User not found' }); } - const product = await productRepository.findOne({ - where: { id: productId }, - }); if (!product) { return res.status(404).json({ message: 'Product not found' }); } @@ -75,18 +76,17 @@ export const AddItemInWishList = [ }), ]; -const RemoveProductRules = [ +const removeProductRules = [ check('productId').isLength({ min: 1 }).withMessage('Product ID is required'), ]; export const RemoveProductFromWishList = [ - ...RemoveProductRules, + ...removeProductRules, errorHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } - const userId = req.user?.id; const { productId } = req.body; @@ -131,7 +131,6 @@ export const getAllWishList = errorHandler( }, relations: ['user', 'product'], }); - return res .status(200) .json({ message: 'Data retrieved successfully', data: wishList }); @@ -142,9 +141,8 @@ export const getOneWishList = errorHandler( async (req: Request, res: Response) => { const userId = req.user?.id; if (!userId) { - return res.status(404).json({ message: 'User ID not found' }); + return res.status(404).json({ message: 'Data Id Not Found' }); } - const wishList = await buyerWishListRepository.findOne({ where: { user: { id: userId } }, relations: ['product'], diff --git a/src/controller/cartController.ts b/src/controller/cartController.ts index 274b2b3a..076bf481 100644 --- a/src/controller/cartController.ts +++ b/src/controller/cartController.ts @@ -9,6 +9,9 @@ import { Order } from '../database/models/orderEntity'; import { OrderDetails } from '../database/models/orderDetailsEntity'; import { check, validationResult } from 'express-validator'; + +import {eventEmitter} from '../Notification.vendor/event.services' + const cartRepository = dbConnection.getRepository(Cart); const productRepository = dbConnection.getRepository(Product); const userRepository = dbConnection.getRepository(UserModel); @@ -62,7 +65,9 @@ export const addToCart = errorHandler(async (req: Request, res: Response) => { newItem.quantity = quantity; const savedItem = await cartRepository.save(newItem); - + + eventEmitter.emit('addToCart',productId, userId) + return res .status(201) .json({ msg: 'Item added to cart successfully', cartItem: savedItem }); @@ -145,6 +150,8 @@ export const removeItem = errorHandler(async (req: Request, res: Response) => { const cartItem = await cartRepository.findOne({ where: { id: itemId }, + select:{user:{id:true}, product:{id:true}}, + relations: ['user', 'product'], }); if (!cartItem) { @@ -152,6 +159,8 @@ export const removeItem = errorHandler(async (req: Request, res: Response) => { } const deletedItem = await cartRepository.delete(itemId); + eventEmitter.emit('removeItem', cartItem) + return res.status(200).json({ msg: 'Cart Item deleted successfully', count: deletedItem.affected, @@ -224,6 +233,7 @@ export const checkout = [ orderDetail.price = price; orderDetails.push(orderDetail); + } // Ensure totalAmount is an integer @@ -240,6 +250,8 @@ export const checkout = [ order.orderDetails = orderDetails; const savedOrder = await orderRepository.save(order); + + eventEmitter.emit('pressorder', order) await cartRepository.delete({ user: { id: userId } }); @@ -280,8 +292,11 @@ export const cancelOrder = errorHandler(async (req: Request, res: Response) => { if (!order) { return res.status(404).json({ msg: 'Order not found' }); } - + eventEmitter.emit('order_canceled', orderId) + await orderRepository.remove(order); + + return res.status(200).json({ msg: 'Order canceled successfully' }); }); diff --git a/src/controller/notificationController.ts b/src/controller/notificationController.ts new file mode 100644 index 00000000..66be0365 --- /dev/null +++ b/src/controller/notificationController.ts @@ -0,0 +1,73 @@ +import { Request, Response } from 'express'; +import Notification_box from '../database/models/inbox_notification'; +import dbConnection from '../database'; +import errorHandler from '../middlewares/errorHandler'; +const NotificationRepository = dbConnection.getRepository(Notification_box) + + + +export const getallNotification= errorHandler( + async(req:Request, res:Response)=>{ + + const notification = await NotificationRepository.find() + + if(!notification) + { + return res.status(400).json({msg:'notification is empty'}) + } + + + return res.status(200).json({msg:'Notification retrieved successfully', notification}) + +} +) + +export const deleteallNotification = errorHandler( + async (req: Request , res: Response)=>{ + const Notication = await NotificationRepository.find() + if(!Notication) + { + return res.status(400).json({msg:'notification is empty'}) + } + + await NotificationRepository.remove(Notication) + + return res.status(200).json({msg:'All notifications deleted successfully'}) + } + +) + +export const deletenotification = errorHandler( + async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id); + + const notification = await NotificationRepository.findOne({ + where: { notification_id: id } + }); + + + if (!notification) { + return res.status(400).json({ msg: 'No Notifications was found' }); + } + + await NotificationRepository.remove(notification); + return res.status(200).json({ msg: 'Notification removed successfully' }); + } +); + +export const getvendorNotifications = errorHandler( + async (req: Request, res:Response)=>{ + + const id:number= parseInt(req.params.id) + + const notification = await NotificationRepository.find({ + where:{vendor_id:id}}) + + if(!notification) + { + return res.status(400).json({msg: 'No Notifications was found'}) + } + + return res.status(200).json({msg:'Notifications retrieved successfully',notification}) + } +) \ No newline at end of file diff --git a/src/controller/orderController.ts b/src/controller/orderController.ts index 82e60027..e4903c68 100644 --- a/src/controller/orderController.ts +++ b/src/controller/orderController.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import dbConnection from '../database'; import errorHandler from '../middlewares/errorHandler'; import { Order } from '../database/models/orderEntity'; - +import {eventEmitter} from '../Notification.vendor/event.services' const orderRepository = dbConnection.getRepository(Order); export const updateOrderStatus = errorHandler( @@ -40,8 +40,11 @@ export const updateOrderStatus = errorHandler( } order.status = status; + await orderRepository.save(order); + eventEmitter.emit('order_status_change',order.id) + return res .status(200) .json({ msg: `Order status updated to ${order.status}` }); diff --git a/src/controller/productController.ts b/src/controller/productController.ts index ec29705d..d8b7e83b 100644 --- a/src/controller/productController.ts +++ b/src/controller/productController.ts @@ -7,6 +7,8 @@ import { check, validationResult } from 'express-validator'; import errorHandler from '../middlewares/errorHandler'; import productQuantityWatch from '../middlewares/productAvailabilityWatch'; +import {eventEmitter} from '../Notification.vendor/event.services' + const userRepository = dbConnection.getRepository(UserModel); const productRepository = dbConnection.getRepository(Product); @@ -126,6 +128,9 @@ export const createProduct = [ type, }); const updatedProduct = await productRepository.save(newProduct); + + eventEmitter.emit('productCreated', updatedProduct) + return res.status(201).json({ message: 'Product successfully created', data: updatedProduct, @@ -217,6 +222,9 @@ export const updateProduct = [ product.isAvailable = isAvailable; const updatedProduct = await productRepository.save(product); + + eventEmitter.emit('product_updated', updatedProduct) + await productQuantityWatch(updatedProduct); return res.status(200).json({ message: 'Product successfully updated', @@ -288,8 +296,10 @@ export const deleteProduct = errorHandler( return res.status(404).json({ message: 'Product Not Found' }); } + eventEmitter.emit('product_deleted', productId) + await productRepository.delete(productId); - + return res.status(200).json({ message: 'Product deleted successfully' }); } ); @@ -361,7 +371,7 @@ export const AvailableProducts = errorHandler( where: { isAvailable: true }, take: limit, skip: (page - 1) * limit, - select: { vendor: { firstName: true, lastName: true, picture: true } }, + select: { vendor: { firstName: true, lastName: true, picture: true, id:true, email:true} }, relations: ['category', 'vendor'], }); diff --git a/src/database/models/inbox_notification.ts b/src/database/models/inbox_notification.ts new file mode 100644 index 00000000..f31d9069 --- /dev/null +++ b/src/database/models/inbox_notification.ts @@ -0,0 +1,32 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from 'typeorm'; + + +@Entity() +export default class Notification_box { + @PrimaryGeneratedColumn() + notification_id: number; + + @Column() + message_title:string; + + @Column() + message_content: string; + + @Column() + product_id: number; + + @Column() + vendor_id: number; + + @Column() + vendor_email:string; + + @Column({ default: false }) + isRead: boolean; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt:Date +} diff --git a/src/database/models/index.ts b/src/database/models/index.ts index d0916eba..d3ab2119 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -2,3 +2,4 @@ export * from './userModel'; export * from './roleEntity'; export * from './productEntity'; export * from './cartEntity'; +export * from './inbox_notification' \ No newline at end of file diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index c2716a3a..dd38a53a 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -156,17 +156,152 @@ /** * @swagger - * /api/v1/buyer/getOneWishList: - * get: - * summary: Get One Wish List + * /api/v1/buyer/payment: + * post: + * summary: Create a charge * tags: [Buyer] * security: * - bearerAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * token: + * type: string + * description: Stripe token + * orderId: + * type: number + * description: Order ID + * required: + * - token + * - orderId + * responses: + * '202': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the charge was successful + * charge: + * type: object + * description: The charge object returned by Stripe + * required: + * - success + * - charge + * '400': + * description: Invalid input or order has already been paid + * '404': + * description: Order not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/buyer/momoPay: + * post: + * summary: Pay order using momo + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * momoNumber: + * type: string + * description: Mobile Money Number + * orderId: + * type: number + * description: Order ID + * required: + * - momoNumber + * - orderId * responses: * '200': - * description: Successful + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: the charge was successful + * message: + * description: 'Transaction Accepted' + * requestId: + * type: string + * required: + * - success + * - message + * - requestId + * '400': + * description: Invalid input or order has already been paid * '404': - * description: Product not found + * description: Order not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/buyer/getPaymentStatus/{id}: + * post: + * summary: Get Payment Status + * tags: [Buyer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * schema: + * type: number + * required: true + * description: Order Id + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * requestId: + * type: string + * description: Request Id For Payment + * required: + * - requestId + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates whether the operation was successful + * message: + * type: string + * description: Message describing the result of the operation + * requestId: + * type: string + * description: Request Id For Payment + * required: + * - success + * - message + * - requestId + * '400': + * description: Invalid input or order has already been paid + * '404': + * description: Order not found * '500': * description: Internal Server Error */ diff --git a/src/docs/notification.vendor.Docs.ts b/src/docs/notification.vendor.Docs.ts new file mode 100644 index 00000000..2b16bbe3 --- /dev/null +++ b/src/docs/notification.vendor.Docs.ts @@ -0,0 +1,86 @@ + +/** + * @swagger + * tags: + * name: Notifications + * description: API endpoints for managing notifications + */ + +/** + * @swagger + * /api/v1/notification/vendor: + * get: + * summary: Get all vendors notifications + * tags: [Notifications] + * responses: + * '200': + * description: A list of notifications + * content: + * application/json: + * schema: + * type: object + * properties: + * notification: + * type: array + * items: + * $ref: '#/components/schemas/Notification' + */ + + +/** + * @swagger + * /api/v1/notification/vendor: + * delete: + * summary: Delete all vendors notifications + * tags: [Notifications] + * responses: + * '200': + * description: All notifications deleted successfully + */ + + +/** + * @swagger + * /api/v1/notification/vendor/{id}: + * get: + * summary: Get notifications by vendor ID + * tags: [Notifications] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Vendor ID + * responses: + * '200': + * description: Notifications for the vendor + * content: + * application/json: + * schema: + * type: object + * properties: + * notification: + * type: array + * items: + * $ref: '#/components/schemas/Notification' + */ + + +/** + * @swagger + * /api/v1/notification/vendor/{id}: + * delete: + * summary: Delete a vendor notification by ID + * tags: [Notifications] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Notification ID + * responses: + * '200': + * description: Notification deleted successfully + */ \ No newline at end of file diff --git a/src/docs/subscribe.ts b/src/docs/subscribe.ts new file mode 100644 index 00000000..511af5ac --- /dev/null +++ b/src/docs/subscribe.ts @@ -0,0 +1,87 @@ +/** + * @swagger + * tags: + * name: Subscribe + * description: Subscription management + */ + +/** + * @openapi + * /api/v1/subscribe: + * post: + * tags: [Subscribe] + * summary: Subscribe user to our app + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * example: test@gmail.com + * responses: + * 201: + * description: Subscribed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscribed successfully + * subscription: + * type: object + * properties: + * email: + * type: string + * example: test@gmail.com + */ + +/** + * @openapi + * /api/v1/subscribe/delete/{id}: + * delete: + * tags: [Subscribe] + * summary: Removes a user from subscription + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * example: 1 + * responses: + * 200: + * description: Subscription removed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscription removed successfully + * 404: + * description: Subscription not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscription not found + * 400: + * description: Invalid ID + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Invalid ID + */ diff --git a/src/index.ts b/src/index.ts index 7e2e3c2b..ae24f298 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import app from './app'; import { DbConnection } from './database'; - +import cron_tasks from '../src/Notification.vendor/node.cron.services' declare module 'express' { export interface Request { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -14,5 +14,8 @@ const PORT = process.env.PORT; (async () => { // connecting to the database await DbConnection.instance.initializeDb(); + app.listen(PORT, () => console.log(`App is up and listening to ${PORT}`)); + + cron_tasks.start() })(); diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index a4cd06a2..3d741642 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -10,7 +10,6 @@ function errorHandler(func: MiddlewareFunction): MiddlewareFunction { try { return await func(req, res); } catch (error) { - // console.log({'Error':error}) const message = (error as { detail?: string }).detail || 'Internal Server Error'; return res.status(500).send(message); diff --git a/src/routes/buyerRoutes.ts b/src/routes/buyerRoutes.ts index 0952c5af..cd2255b1 100644 --- a/src/routes/buyerRoutes.ts +++ b/src/routes/buyerRoutes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { checkRole } from '../middlewares/authorize'; -import { getOneProduct } from '../controller/buyerController'; +import { + MomohandlePayment, + checkPaymentStatus, + getOneProduct, +} from '../controller/buyerController'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; import { AddItemInWishList, @@ -22,5 +26,7 @@ buyerRouter.get('/getWishList', IsLoggedIn, getAllWishList); buyerRouter.get('/getOneWishList', IsLoggedIn, getOneWishList); buyerRouter.post('/payment', handlePayment); +buyerRouter.post('/momoPay', MomohandlePayment); +buyerRouter.post('/getPaymentStatus/:id', checkPaymentStatus); export default buyerRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index 5060e11a..71bb400b 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -9,7 +9,7 @@ import couponRouter from './couponRoute'; import chekoutRoutes from './checkoutRoutes'; import reviewRoute from './reviewRoutes'; import orderRoutes from './orderRoutes'; - +import noticificationRoute from './notificationRoutes' const router = Router(); router.use('/user', userRouter); @@ -22,5 +22,5 @@ router.use('/coupons', couponRouter); router.use('/checkout', chekoutRoutes); router.use('/review', reviewRoute); router.use('/order', orderRoutes); - -export default router; \ No newline at end of file +router.use('/notification',noticificationRoute) +export default router; diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts new file mode 100644 index 00000000..1b2b184e --- /dev/null +++ b/src/routes/notificationRoutes.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { checkRole } from '../middlewares/authorize'; +import { + getallNotification, + deleteallNotification, + deletenotification, + getvendorNotifications} from '../controller/notificationController' +const notificationRouter = Router(); + +notificationRouter.route('/vendor') + .get(getallNotification) + .delete(deleteallNotification) +notificationRouter.route('/vendor/:id') + .delete(IsLoggedIn,checkRole(['Vendor']),deletenotification) + .get(IsLoggedIn,checkRole(['Vendor']),getvendorNotifications) + +export default notificationRouter \ No newline at end of file