diff --git a/package-lock.json b/package-lock.json index 1c49d78f..6a519a63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "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", @@ -52,6 +52,7 @@ "@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", @@ -67,6 +68,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", @@ -1881,6 +1883,12 @@ "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", @@ -2569,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", @@ -2853,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" @@ -3242,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" @@ -3391,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", @@ -3466,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", @@ -3585,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", @@ -3637,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", @@ -3987,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", @@ -4025,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", @@ -4398,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" @@ -4581,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", @@ -4635,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", @@ -4672,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", @@ -4697,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" }, @@ -4901,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", @@ -5063,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", @@ -5413,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", @@ -5571,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" ] @@ -5606,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", @@ -6886,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", @@ -7214,19 +6971,32 @@ "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/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": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -7607,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", @@ -8195,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", @@ -8236,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", @@ -8329,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", @@ -8635,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", @@ -8840,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", @@ -9372,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", @@ -10191,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" } @@ -10263,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", @@ -10290,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 eed92116..fdb7ec4d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "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", @@ -69,13 +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": { @@ -88,6 +90,7 @@ "@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", @@ -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 { + 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__/subscription.test.ts b/src/__test__/subscription.test.ts new file mode 100644 index 00000000..dfa18493 --- /dev/null +++ b/src/__test__/subscription.test.ts @@ -0,0 +1,97 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import dbConnection from '../database'; +import Subscription from '../database/models/Subscribe'; + +const subscribeRepository = dbConnection.getRepository(Subscription); + +beforeAll(async () => { + await beforeAllHook(); +}); + +afterAll(async () => { + await afterAllHook(); +}); + +describe('POST /api/v1/subscribe', () => { + beforeEach(async () => { + // Clear the table before each test + await subscribeRepository.clear(); + }); + + it('should subscribe successfully with a valid email', async () => { + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'test@example.com' }); + + expect(response.status).toBe(201); + expect(response.body.message).toBe('Subscribed successfully'); + expect(response.body.subscription.email).toBe('test@example.com'); + + const subscription = await subscribeRepository.findOne({ + where: { email: 'test@example.com' }, + }); + expect(subscription).toBeDefined(); + }); + + it('should return 400 if the email is already subscribed', async () => { + const subscription = new Subscription(); + subscription.email = 'test@example.com'; + await subscribeRepository.save(subscription); + + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'test@example.com' }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Email is already subscribed'); + }); + + it('should return 400 for an invalid email format', async () => { + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'invalid-email' }); + + expect(response.status).toBe(400); + expect(response.body.errors[0].msg).toBe('Email is not valid'); + }); +}); + +describe('DELETE /api/v1/subscribe/delete/:id', () => { + beforeEach(async () => { + // Clear the table before each test + await subscribeRepository.clear(); + }); + + it('should remove a subscription successfully', async () => { + const subscription = new Subscription(); + subscription.email = 'test@example.com'; + await subscribeRepository.save(subscription); + + const response = await request(app) + .delete(`/api/v1/subscribe/delete/${subscription.id}`) + .send(); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Subscription removed successfully'); + }); + + it('should return 404 if the subscription does not exist', async () => { + const response = await request(app) + .delete('/api/v1/subscribe/delete/450') + .send(); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Subscription not found'); + }); + + it('should return 400 for invalid ID', async () => { + const response = await request(app) + .delete('/api/v1/subscribe/delete/noid') + .send(); + + expect(response.status).toBe(400); + expect(response.body.message).toBeUndefined(); + }); +}); 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/controller/subscribeController.ts b/src/controller/subscribeController.ts new file mode 100644 index 00000000..88f6fb06 --- /dev/null +++ b/src/controller/subscribeController.ts @@ -0,0 +1,64 @@ +import { Request, Response } from 'express'; +import errorHandler from '../middlewares/errorHandler'; +import dbConnection from '../database'; +import Subscription from '../database/models/Subscribe'; +import { check, validationResult } from 'express-validator'; + +const subscribeRepository = dbConnection.getRepository(Subscription); +const userEmailRules = [ + check('email').isEmail().normalizeEmail().withMessage('Email is not valid'), +]; +export const subscribe = [ + ...userEmailRules, + errorHandler(async (req: Request, res: Response) => { + const { email } = req.body; + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const alreadSubscribed = await subscribeRepository.findOneBy({ + email: req.body.email, + }); + if (alreadSubscribed) { + return res.status(400).json({ message: 'Email is already subscribed' }); + } + + const subscription = new Subscription(); + subscription.email = email; + + await subscribeRepository.save(subscription); + res.status(201).json({ message: 'Subscribed successfully', subscription }); + }), +]; + +const userIdRules = [ + check('id').isInt({ min: 1 }).withMessage(' ID is required'), +]; + +export const removeSubscriber = [ + ...userIdRules, + errorHandler(async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + try { + const subscription = await subscribeRepository.findOne({ + where: { id }, + }); + + if (!subscription) { + return res.status(404).json({ message: 'Subscription not found' }); + } + + await subscribeRepository.remove(subscription); + res.status(200).json({ message: 'Subscription removed successfully' }); + } catch (error) { + res.status(500).json({ message: 'Error removing subscription', error }); + } + }), +]; diff --git a/src/database/models/Subscribe.ts b/src/database/models/Subscribe.ts new file mode 100644 index 00000000..7bc4bbfd --- /dev/null +++ b/src/database/models/Subscribe.ts @@ -0,0 +1,12 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { IsEmail } from 'class-validator'; + +@Entity() +export default class Subscription { + @PrimaryGeneratedColumn() + id: number; + + @Column() + @IsEmail() + email: string; +} 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/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/emails/index.ts b/src/emails/index.ts index 8f88ef6f..5efc3eb5 100644 --- a/src/emails/index.ts +++ b/src/emails/index.ts @@ -1,10 +1,10 @@ import axios from 'axios'; import handlebars from 'handlebars'; import fs from 'fs'; -type EmailType = 'confirm' | 'reset'; +type EmailType = 'confirm' | 'reset' | 'subscription'; type Data = { - name: string; - link: string; + name?: string; + link?: string; }; /** * Sends an email of the specified type to the recipient using the provided data. @@ -15,7 +15,7 @@ type Data = { * @returns A Promise that resolves to the response from the email service. * @throws An error if there is an issue sending the email. */ -async function sendEmail(emailType: EmailType, recipient: string, data: Data) { +async function sendEmail(emailType: EmailType, recipient: string, data?: Data) { const templatePath = `./src/emails/templates/${emailType}.html`; try { // Read the Handlebars template file 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/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 diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 6b64745f..e25f75be 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -17,6 +17,7 @@ import { } from '../controller/changestatusController'; import { checkRole } from '../middlewares/authorize'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { subscribe, removeSubscriber } from '../controller/subscribeController'; const userRouter = Router(); userRouter.post('/register', registerUser); @@ -39,7 +40,9 @@ userRouter.put( deactivateAccount ); userRouter.post('/recover', recoverPassword); -userRouter.put('/recover/confirm', updateNewPassword) +userRouter.put('/recover/confirm', updateNewPassword); -userRouter.put('/updateProfile/:id',updateProfile); +userRouter.put('/updateProfile/:id', updateProfile); +userRouter.post('/subscribe', subscribe); +userRouter.delete('/subscribe/delete/:id', removeSubscriber); export default userRouter;