diff --git a/package-lock.json b/package-lock.json index 2384758..77ff993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@google/generative-ai": "^0.24.1", "@types/nodemailer": "^7.0.4", "@types/uuid": "^8.3.4", + "archiver": "^7.0.1", "axios": "^1.7.9", "bcryptjs": "^2.4.3", "bullmq": "^5.63.0", @@ -54,6 +55,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@types/archiver": "^7.0.0", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.17", @@ -1374,37 +1376,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2421,71 +2392,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", @@ -2523,150 +2429,6 @@ "@napi-rs/canvas-win32-x64-msvc": "0.1.80" } }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", - "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", - "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", - "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", - "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", - "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", - "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", - "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", - "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", - "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { "version": "0.1.80", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", @@ -2683,278 +2445,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/snappy-android-arm-eabi": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.3.3.tgz", - "integrity": "sha512-d4vUFFzNBvazGfB/KU8MnEax6itTIgRWXodPdZDnWKHy9HwVBndpCiedQDcSNHcZNYV36rx034rpn7SAuTL2NA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-android-arm64": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.3.3.tgz", - "integrity": "sha512-Uh+w18dhzjVl85MGhRnojb7OLlX2ErvMsYIunO/7l3Frvc2zQvfqsWsFJanu2dwqlE2YDooeNP84S+ywgN9sxg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-darwin-arm64": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.3.3.tgz", - "integrity": "sha512-AmJn+6yOu/0V0YNHLKmRUNYkn93iv/1wtPayC7O1OHtfY6YqHQ31/MVeeRBiEYtQW9TwVZxXrDirxSB1PxRdtw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-darwin-x64": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.3.3.tgz", - "integrity": "sha512-biLTXBmPjPmO7HIpv+5BaV9Gy/4+QJSUNJW8Pjx1UlWAVnocPy7um+zbvAWStZssTI5sfn/jOClrAegD4w09UA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-freebsd-x64": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.3.3.tgz", - "integrity": "sha512-E3R3ewm8Mrjm0yL2TC3VgnphDsQaCPixNJqBbGiz3NTshVDhlPlOgPKF0NGYqKiKaDGdD9PKtUgOR4vagUtn7g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.3.3.tgz", - "integrity": "sha512-ZuNgtmk9j0KyT7TfLyEnvZJxOhbkyNR761nk04F0Q4NTHMICP28wQj0xgEsnCHUsEeA9OXrRL4R7waiLn+rOQA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-arm64-gnu": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.3.3.tgz", - "integrity": "sha512-KIzwtq0dAzshzpqZWjg0Q9lUx93iZN7wCCUzCdLYIQ+mvJZKM10VCdn0RcuQze1R3UJTPwpPLXQIVskNMBYyPA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-arm64-musl": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.3.3.tgz", - "integrity": "sha512-AAED4cQS74xPvktsyVmz5sy8vSxG/+3d7Rq2FDBZzj3Fv6v5vux6uZnECPCAqpALCdTtJ61unqpOyqO7hZCt1Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-ppc64-gnu": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-ppc64-gnu/-/snappy-linux-ppc64-gnu-7.3.3.tgz", - "integrity": "sha512-pofO5eSLg8ZTBwVae4WHHwJxJGZI8NEb4r5Mppvq12J/1/Hq1HecClXmfY3A7bdT2fsS2Td+Q7CI9VdBOj2sbA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-riscv64-gnu": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-riscv64-gnu/-/snappy-linux-riscv64-gnu-7.3.3.tgz", - "integrity": "sha512-OiHYdeuwj0TVBXADUmmQDQ4lL1TB+8EwmXnFgOutoDVXHaUl0CJFyXLa6tYUXe+gRY8hs1v7eb0vyE97LKY06Q==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-s390x-gnu": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-s390x-gnu/-/snappy-linux-s390x-gnu-7.3.3.tgz", - "integrity": "sha512-66QdmuV9CTq/S/xifZXlMy3PsZTviAgkqqpZ+7vPCmLtuP+nqhaeupShOFf/sIDsS0gZePazPosPTeTBbhkLHg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-x64-gnu": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.3.3.tgz", - "integrity": "sha512-g6KURjOxrgb8yXDEZMuIcHkUr/7TKlDwSiydEQtMtP3n4iI4sNjkcE/WNKlR3+t9bZh1pFGAq7NFRBtouQGHpQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-linux-x64-musl": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.3.3.tgz", - "integrity": "sha512-6UvOyczHknpaKjrlKKSlX3rwpOrfJwiMG6qA0NRKJFgbcCAEUxmN9A8JvW4inP46DKdQ0bekdOxwRtAhFiTDfg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-openharmony-arm64": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-openharmony-arm64/-/snappy-openharmony-arm64-7.3.3.tgz", - "integrity": "sha512-I5mak/5rTprobf7wMCk0vFhClmWOL/QiIJM4XontysnadmP/R9hAcmuFmoMV2GaxC9MblqLA7Z++gy8ou5hJVw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-wasm32-wasi": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-wasm32-wasi/-/snappy-wasm32-wasi-7.3.3.tgz", - "integrity": "sha512-+EroeygVYo9RksOchjF206frhMkfD2PaIun3yH4Zp5j/Y0oIEgs/+VhAYx/f+zHRylQYUIdLzDRclcoepvlR8Q==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@napi-rs/snappy-win32-arm64-msvc": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.3.3.tgz", - "integrity": "sha512-rxqfntBsCfzgOha/OlG8ld2hs6YSMGhpMUbFjeQLyVDbooY041fRXv3S7yk52DfO6H4QQhLT5+p7cW0mYdhyiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/snappy-win32-ia32-msvc": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.3.3.tgz", - "integrity": "sha512-joRV16DsRtqjGt0CdSpxGCkO0UlHGeTZ/GqvdscoALpRKbikR2Top4C61dxEchmOd3lSYsXutuwWWGg3Nr++WA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@napi-rs/snappy-win32-x64-msvc": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.3.3.tgz", @@ -2971,18 +2461,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3821,14 +3299,14 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@types/readdir-glob": "*" } }, "node_modules/@types/babel__core": { @@ -4167,6 +3645,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/request": { "version": "2.48.13", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", @@ -4745,88 +4233,101 @@ "license": "MIT" }, "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "license": "MIT", "dependencies": { - "archiver-utils": "^2.1.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "license": "MIT", "dependencies": { - "glob": "^7.1.4", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", + "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/archiver/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/arg": { @@ -5319,6 +4820,30 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5469,9 +4994,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -5489,7 +5014,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-crc32": { @@ -5916,18 +5441,35 @@ } }, "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "license": "MIT", "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/concat-map": { @@ -6163,16 +5705,32 @@ } }, "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/create-jest": { @@ -7116,6 +6674,15 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -7145,6 +6712,154 @@ "node": ">=8.3.0" } }, + "node_modules/exceljs/node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/exceljs/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/exceljs/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "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/exceljs/node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/exceljs/node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/exceljs/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==", + "license": "MIT" + }, + "node_modules/exceljs/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/exceljs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/exceljs/node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/exceljs/node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7708,21 +7423,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -11338,6 +11038,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -13984,38 +13693,33 @@ } }, "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "license": "MIT", "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/zod": { diff --git a/package.json b/package.json index b39d25f..0d1f309 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@google/generative-ai": "^0.24.1", "@types/nodemailer": "^7.0.4", "@types/uuid": "^8.3.4", + "archiver": "^7.0.1", "axios": "^1.7.9", "bcryptjs": "^2.4.3", "bullmq": "^5.63.0", @@ -78,6 +79,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@types/archiver": "^7.0.0", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.17", diff --git a/src/controllers/form16.controller.ts b/src/controllers/form16.controller.ts index 7287c70..96a7878 100644 --- a/src/controllers/form16.controller.ts +++ b/src/controllers/form16.controller.ts @@ -199,6 +199,58 @@ export class Form16Controller { } } + /** + * GET /api/v1/form16/uploaded-form16-pdfs + * RE only. List uploaded Form16A PDFs that are successfully submitted and have credit notes. + */ + async listUploadedForm16Pdfs(req: Request, res: Response): Promise { + try { + const result = await form16Service.listUploadedForm16Pdfs({ + search: req.query.search as string | undefined, + dealerCode: req.query.dealerCode as string | undefined, + dealerName: req.query.dealerName as string | undefined, + financialYear: req.query.financialYear as string | undefined, + assessmentYear: req.query.assessmentYear as string | undefined, + quarter: req.query.quarter as string | undefined, + uploadedFrom: req.query.uploadedFrom as string | undefined, + uploadedTo: req.query.uploadedTo as string | undefined, + zone: req.query.zone as string | undefined, + region: req.query.region as string | undefined, + limit: req.query.limit != null ? parseInt(String(req.query.limit), 10) : undefined, + offset: req.query.offset != null ? parseInt(String(req.query.offset), 10) : undefined, + }); + return ResponseHandler.success(res, result, 'Uploaded Form 16 PDFs fetched'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('[Form16Controller] listUploadedForm16Pdfs error:', error); + return ResponseHandler.error(res, 'Failed to fetch uploaded Form 16 PDFs', 500, errorMessage); + } + } + + /** + * POST /api/v1/form16/uploaded-form16-pdfs/bulk-download-links + * RE only. Return document links for selected submission ids (used by FE bulk download action). + */ + async getUploadedForm16PdfBulkDownloadLinks(req: Request, res: Response): Promise { + try { + const idsRaw = Array.isArray((req.body as any)?.submissionIds) ? (req.body as any).submissionIds : []; + const requested = idsRaw + .map((v: unknown) => Number(v)) + .filter((n: number) => Number.isInteger(n) && n > 0); + if (!requested.length) { + return ResponseHandler.error(res, 'submissionIds is required', 400); + } + + const files = await form16Service.getUploadedForm16PdfLinksBySubmissionIds(requested); + + return ResponseHandler.success(res, { files }, 'Bulk download links generated'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('[Form16Controller] getUploadedForm16PdfBulkDownloadLinks error:', error); + return ResponseHandler.error(res, 'Failed to prepare bulk download links', 500, errorMessage); + } + } + /** * GET /api/v1/form16/dealer/submissions * Dealer only. List Form 16 submissions for the authenticated dealer (pending/failed for Pending Submissions page). @@ -348,6 +400,11 @@ export class Form16Controller { async get26asDashboard(req: Request, res: Response): Promise { try { const dashboard = await form16Service.getForm16DashboardData(); + logger.info('[Form16Controller] 26AS dashboard served', { + yearRows: dashboard.yearWise?.length || 0, + zoneRows: dashboard.zoneWise?.length || 0, + zoneHierarchy: dashboard.zoneHierarchy?.length || 0, + }); return ResponseHandler.success(res, dashboard, 'Form16A dashboard fetched'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; diff --git a/src/migrations/20260427192000-add-form16-dashboard-performance-indexes.ts b/src/migrations/20260427192000-add-form16-dashboard-performance-indexes.ts new file mode 100644 index 0000000..cab7611 --- /dev/null +++ b/src/migrations/20260427192000-add-form16-dashboard-performance-indexes.ts @@ -0,0 +1,50 @@ +import { QueryInterface } from 'sequelize'; + +module.exports = { + up: async (queryInterface: QueryInterface) => { + // Speeds latest-submission window ordering per dealer+FY+quarter. + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_form16a_submissions_dashboard_latest + ON form16a_submissions ( + dealer_code, + financial_year, + quarter, + version DESC, + submitted_date DESC, + created_at DESC, + id DESC + ); + `); + + // Speeds join between latest submissions and 26AS quarter snapshots. + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_form16a_submissions_tan_fy_quarter + ON form16a_submissions (tan_number, financial_year, quarter); + `); + + // Speeds normalized active dealer lookup used by dashboard aggregation. + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_dealers_active_normalized_code_region + ON dealers ( + (TRIM(COALESCE(NULLIF(sales_code, ''), NULLIF(dlrcode, '')))), + region + ) + WHERE is_active = true; + `); + }, + + down: async (queryInterface: QueryInterface) => { + await queryInterface.sequelize.query(` + DROP INDEX IF EXISTS idx_dealers_active_normalized_code_region; + `); + + await queryInterface.sequelize.query(` + DROP INDEX IF EXISTS idx_form16a_submissions_tan_fy_quarter; + `); + + await queryInterface.sequelize.query(` + DROP INDEX IF EXISTS idx_form16a_submissions_dashboard_latest; + `); + }, +}; + diff --git a/src/routes/form16.routes.ts b/src/routes/form16.routes.ts index edd1ab5..dc90197 100644 --- a/src/routes/form16.routes.ts +++ b/src/routes/form16.routes.ts @@ -108,6 +108,18 @@ router.get( requireForm16SubmissionAccess, asyncHandler(form16Controller.listDebitNotes.bind(form16Controller)) ); +router.get( + '/uploaded-form16-pdfs', + requireForm16ReOnly, + requireForm16SubmissionAccess, + asyncHandler(form16Controller.listUploadedForm16Pdfs.bind(form16Controller)) +); +router.post( + '/uploaded-form16-pdfs/bulk-download-links', + requireForm16ReOnly, + requireForm16SubmissionAccess, + asyncHandler(form16Controller.getUploadedForm16PdfBulkDownloadLinks.bind(form16Controller)) +); router.get( '/debit-notes/:id/sap-response', requireForm16ReOnly, diff --git a/src/services/form16.service.ts b/src/services/form16.service.ts index d265a76..1023275 100644 --- a/src/services/form16.service.ts +++ b/src/services/form16.service.ts @@ -1836,11 +1836,21 @@ export async function listDealerPendingQuarters(userId: string) { `SELECT financial_year, quarter, MIN(created_at) AS min_created FROM tds_26as_entries GROUP BY financial_year, quarter`, { type: QueryTypes.SELECT } )); + const twentySixAsAmounts = (await sequelize.query<{ financial_year: string; quarter: string; total_amount: number | string }>( + `SELECT financial_year, quarter, COALESCE(SUM(tax_deducted), 0) AS total_amount FROM tds_26as_entries GROUP BY financial_year, quarter`, + { type: QueryTypes.SELECT } + )); const minDateByKey = new Map(); for (const row of twentySixAsMinDates) { const key = `${row.financial_year}|${row.quarter}`; if (row.min_created) minDateByKey.set(key, new Date(row.min_created)); } + const amountByKey = new Map(); + for (const row of twentySixAsAmounts) { + const key = `${row.financial_year}|${row.quarter}`; + const amount = Number(row.total_amount ?? 0); + amountByKey.set(key, Number.isFinite(amount) ? amount : 0); + } const dealerSubmissions = await Form16aSubmission.findAll({ where: { dealerCode }, @@ -1877,6 +1887,7 @@ export async function listDealerPendingQuarters(userId: string) { days_remaining: number | null; days_overdue: number | null; days_since_26as_uploaded: number | null; + twenty_six_as_amount: number; }> = []; const now = new Date(); const oneDayMs = 24 * 60 * 60 * 1000; @@ -1901,6 +1912,7 @@ export async function listDealerPendingQuarters(userId: string) { const daysSince26AsUploaded = twentySixAsMin ? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs) : null; + const twentySixAsAmount = amountByKey.get(key) ?? 0; result.push({ financial_year: financialYear, quarter, @@ -1913,6 +1925,7 @@ export async function listDealerPendingQuarters(userId: string) { days_remaining: daysRemaining, days_overdue: daysOverdue, days_since_26as_uploaded: daysSince26AsUploaded, + twenty_six_as_amount: twentySixAsAmount, }); } result.sort((a, b) => { @@ -1925,6 +1938,282 @@ export async function listDealerPendingQuarters(userId: string) { return result; } +export interface Form16UploadedPdfFilters { + search?: string; + dealerCode?: string; + dealerName?: string; + financialYear?: string; + assessmentYear?: string; + quarter?: string; + uploadedFrom?: string; + uploadedTo?: string; + zone?: string; + region?: string; + limit?: number; + offset?: number; +} + +export interface Form16UploadedPdfRow { + submissionId: number; + requestId: string; + creditNoteNumber: string; + dealerName: string; + dealerCode: string; + region: string; + zone: string; + financialYear: string; + assessmentYear: string; + quarter: string; + amount: number; + uploadedDate: string | null; + documentUrl: string; +} + +type Form16UploadedPdfSqlRow = { + submission_id: number; + request_id: string; + credit_note_number: string; + dealer_name: string | null; + dealer_code: string; + region: string | null; + zone: string | null; + financial_year: string; + assessment_year: string | null; + quarter: string; + amount: string | number; + uploaded_date: Date | string | null; + document_url: string; +}; + +/** + * RE only. List uploaded Form16A PDFs that were successfully processed and have credit notes. + * Includes dealer + geography columns and supports filter/search for management views. + */ +export async function listUploadedForm16Pdfs(filters?: Form16UploadedPdfFilters): Promise<{ + rows: Form16UploadedPdfRow[]; + total: number; +}> { + const replacements: Record = { + limit: Math.min(Math.max(Number(filters?.limit || 50), 1), 500), + offset: Math.max(Number(filters?.offset || 0), 0), + }; + const where: string[] = [ + `s.validation_status = 'success'`, + `COALESCE(TRIM(s.document_url), '') <> ''`, + ]; + + const search = String(filters?.search || '').trim().toLowerCase(); + if (search) { + replacements.search = `%${search}%`; + where.push( + `( + LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :search + OR LOWER(COALESCE(s.dealer_code, '')) LIKE :search + OR LOWER(COALESCE(s.financial_year, '')) LIKE :search + OR LOWER(COALESCE(s.quarter, '')) LIKE :search + OR LOWER(COALESCE(c.credit_note_number, '')) LIKE :search + )` + ); + } + + const dealerCode = String(filters?.dealerCode || '').trim(); + if (dealerCode) { + replacements.dealerCode = `%${dealerCode.toLowerCase()}%`; + where.push(`LOWER(COALESCE(s.dealer_code, '')) LIKE :dealerCode`); + } + + const dealerName = String(filters?.dealerName || '').trim(); + if (dealerName) { + replacements.dealerName = `%${dealerName.toLowerCase()}%`; + where.push(`LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :dealerName`); + } + + const financialYear = normalizeFinancialYear(filters?.financialYear || '') || String(filters?.financialYear || '').trim(); + if (financialYear) { + replacements.financialYear = financialYear; + where.push(`s.financial_year = :financialYear`); + } + + const quarter = normalizeQuarter(filters?.quarter || '') || String(filters?.quarter || '').trim().toUpperCase(); + if (quarter) { + replacements.quarter = quarter; + where.push(`s.quarter = :quarter`); + } + + const region = String(filters?.region || '').trim(); + if (region) { + replacements.region = `%${region.toLowerCase()}%`; + where.push(`LOWER(COALESCE(d.region, 'UNKNOWN')) LIKE :region`); + } + + const zone = String(filters?.zone || '').trim(); + if (zone) { + replacements.zone = zone.toUpperCase(); + where.push( + `(CASE + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) IN ('N','S','E','W','C') + THEN UPPER(LEFT(d.region, 1)) + ELSE 'UNKNOWN' + END) = :zone` + ); + } + + const uploadedFrom = String(filters?.uploadedFrom || '').trim(); + if (uploadedFrom) { + replacements.uploadedFrom = uploadedFrom; + where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) >= :uploadedFrom`); + } + + const uploadedTo = String(filters?.uploadedTo || '').trim(); + if (uploadedTo) { + replacements.uploadedTo = uploadedTo; + where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) <= :uploadedTo`); + } + + const assessmentYear = String(filters?.assessmentYear || '').trim(); + if (assessmentYear) { + replacements.assessmentYear = assessmentYear.toLowerCase(); + where.push( + `LOWER( + CASE + WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$' + THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0')) + ELSE s.financial_year + END + ) LIKE '%' || :assessmentYear || '%'` + ); + } + + const whereSql = where.length ? `WHERE ${where.join('\n AND ')}` : ''; + const baseFromSql = ` + FROM form16a_submissions s + INNER JOIN form_16_credit_notes c ON c.submission_id = s.id + LEFT JOIN LATERAL ( + SELECT + d.dealership, + d.dealer_principal_name, + d.region + FROM dealers d + WHERE d.is_active = true + AND ( + COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '') + OR COALESCE(d.dlrcode, '') = COALESCE(s.dealer_code, '') + ) + ORDER BY CASE WHEN COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '') THEN 0 ELSE 1 END + LIMIT 1 + ) d ON true + `; + + const rows = await sequelize.query( + ` + SELECT + s.id AS submission_id, + s.request_id, + c.credit_note_number, + COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code) AS dealer_name, + s.dealer_code, + COALESCE(d.region, 'UNKNOWN') AS region, + CASE + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'N' THEN 'North' + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'S' THEN 'South' + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'E' THEN 'East' + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'W' THEN 'West' + WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'C' THEN 'Central' + ELSE 'Unknown' + END AS zone, + s.financial_year, + CASE + WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$' + THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0')) + ELSE s.financial_year + END AS assessment_year, + s.quarter, + s.total_amount AS amount, + COALESCE(s.submitted_date, s.created_at) AS uploaded_date, + s.document_url + ${baseFromSql} + ${whereSql} + ORDER BY COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC + LIMIT :limit OFFSET :offset + `, + { replacements, type: QueryTypes.SELECT } + ); + + const countRows = await sequelize.query<{ total: string }>( + ` + SELECT COUNT(1)::text AS total + ${baseFromSql} + ${whereSql} + `, + { replacements, type: QueryTypes.SELECT } + ); + + const total = Number(countRows?.[0]?.total || 0); + return { + rows: rows.map((r) => ({ + submissionId: Number(r.submission_id), + requestId: r.request_id, + creditNoteNumber: r.credit_note_number, + dealerName: String(r.dealer_name || r.dealer_code || '—'), + dealerCode: String(r.dealer_code || '—'), + region: String(r.region || 'UNKNOWN'), + zone: String(r.zone || 'Unknown'), + financialYear: String(r.financial_year || ''), + assessmentYear: String(r.assessment_year || r.financial_year || ''), + quarter: String(r.quarter || ''), + amount: Number(r.amount || 0), + uploadedDate: r.uploaded_date ? new Date(r.uploaded_date).toISOString() : null, + documentUrl: String(r.document_url || ''), + })), + total, + }; +} + +export async function getUploadedForm16PdfLinksBySubmissionIds(submissionIds: number[]): Promise> { + const ids = (submissionIds || []).map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0); + if (!ids.length) return []; + const rows = await sequelize.query<{ + submission_id: number; + credit_note_number: string; + dealer_code: string; + quarter: string; + financial_year: string; + document_url: string; + }>( + ` + SELECT + s.id AS submission_id, + c.credit_note_number, + s.dealer_code, + s.quarter, + s.financial_year, + s.document_url + FROM form16a_submissions s + INNER JOIN form_16_credit_notes c ON c.submission_id = s.id + WHERE s.id IN (:ids) + AND s.validation_status = 'success' + AND COALESCE(TRIM(s.document_url), '') <> '' + ORDER BY s.id DESC + `, + { replacements: { ids }, type: QueryTypes.SELECT } + ); + return rows.map((r) => ({ + submissionId: Number(r.submission_id), + creditNoteNumber: r.credit_note_number, + dealerCode: r.dealer_code, + quarter: r.quarter, + financialYear: r.financial_year, + documentUrl: r.document_url, + })); +} + /** * RE only. Cancel a Form 16 submission: set submission status to cancelled and workflow to REJECTED. */ @@ -3118,11 +3407,31 @@ export interface Form16DashboardBreakdownRow { pendingDealerCount: number; } +export interface Form16DashboardDealerRow { + dealerCode: string; + dealerName: string; + regionId: string; + totalAmount: number; + submittedAmount: number; + pendingAmount: number; +} + +export interface Form16DashboardZoneRegionNode extends Form16DashboardBreakdownRow { + regionId: string; + dealers: Form16DashboardDealerRow[]; +} + +export interface Form16DashboardZoneNode extends Form16DashboardBreakdownRow { + zoneCode: string; + regions: Form16DashboardZoneRegionNode[]; +} + export interface Form16DashboardData { kpi: Form16DashboardKpi; overall: Form16DashboardOverall; yearWise: Form16DashboardBreakdownRow[]; zoneWise: Form16DashboardBreakdownRow[]; + zoneHierarchy: Form16DashboardZoneNode[]; } /** @@ -3139,64 +3448,172 @@ export async function getForm16DashboardData(): Promise { return Number.isFinite(n) ? n : 0; }; - const [overallRow] = await sequelize.query<{ - total_amount: number | string | null; - submitted_amount: number | string | null; - total_dealers: number | string | null; - submitted_dealer_count: number | string | null; + const zoneSortRank = (zoneCode: string): number => { + const z = String(zoneCode || '').toUpperCase(); + if (z === 'N') return 1; + if (z === 'C') return 2; + if (z === 'W') return 3; + if (z === 'E') return 4; + if (z === 'S') return 5; + return 99; + }; + + const zoneName = (zoneCode: string): string => { + const z = String(zoneCode || '').toUpperCase(); + if (z === 'N') return 'North'; + if (z === 'C') return 'Central'; + if (z === 'W') return 'West'; + if (z === 'E') return 'East'; + if (z === 'S') return 'South'; + return z || 'Unknown'; + }; + + const regionSortValue = (regionId: string): number => { + const r = String(regionId || '').toUpperCase(); + const m = /^([A-Z]+)(\d+)$/.exec(r); + if (!m) return Number.MAX_SAFE_INTEGER; + return parseInt(m[2], 10); + }; + + // Dealer master is the base universe for dashboard (include all active dealers). + const dealerUniverseRaw = await sequelize.query<{ + dealer_code: string; + dealer_name: string | null; + region_id: string | null; }>( ` - WITH active_dealers AS ( + SELECT DISTINCT + TRIM(COALESCE(d.dlrcode, '')) AS dealer_code, + COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name, + UPPER(TRIM(COALESCE(d.region, ''))) AS region_id + FROM dealers d + WHERE d.is_active = true + AND TRIM(COALESCE(d.dlrcode, '')) <> '' + `, + { type: QueryTypes.SELECT } + ); + + const dealerUniverse = (dealerUniverseRaw || []).map((r) => ({ + dealerCode: String(r.dealer_code || '').trim(), + dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer', + regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN', + })); + + const detailRowsRaw = await sequelize.query<{ + dealer_code: string; + dealer_name: string | null; + region_id: string | null; + financial_year: string; + quarter: string; + total_amount: number | string | null; + submitted_amount: number | string | null; + }>( + ` + WITH dealer_meta AS ( SELECT DISTINCT - TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code + TRIM(COALESCE(d.dlrcode, '')) AS dealer_code, + COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name, + UPPER(TRIM(COALESCE(d.region, ''))) AS region_id FROM dealers d WHERE d.is_active = true - AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> '' + AND TRIM(COALESCE(d.dlrcode, '')) <> '' ), latest_submissions AS ( SELECT s.id, - s.dealer_code, + TRIM(COALESCE(s.dealer_code, '')) AS dealer_code, s.financial_year, s.quarter, + s.tan_number, COALESCE(s.total_amount, 0)::numeric AS total_amount, ROW_NUMBER() OVER ( PARTITION BY s.dealer_code, s.financial_year, s.quarter ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC ) AS rn FROM form16a_submissions s - INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code + WHERE TRIM(COALESCE(s.dealer_code, '')) <> '' ), latest_base AS ( - SELECT id, dealer_code, financial_year, quarter, total_amount + SELECT id, dealer_code, financial_year, quarter, tan_number, total_amount FROM latest_submissions WHERE rn = 1 ), - submitted_by_dealer AS ( - SELECT - lb.dealer_code, - SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount - FROM latest_base lb - LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id - GROUP BY lb.dealer_code + latest_snapshots AS ( + SELECT tan_number, financial_year, quarter, aggregated_amount + FROM ( + SELECT + ss.tan_number, + ss.financial_year, + ss.quarter, + COALESCE(ss.aggregated_amount, 0)::numeric AS aggregated_amount, + ROW_NUMBER() OVER ( + PARTITION BY ss.tan_number, ss.financial_year, ss.quarter + ORDER BY ss.id DESC, ss.created_at DESC + ) AS rn + FROM form_16_26as_quarter_snapshots ss + ) t + WHERE t.rn = 1 + ), + credit_by_submission AS ( + SELECT cn.submission_id, SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount + FROM form_16_credit_notes cn + GROUP BY cn.submission_id ) SELECT - COALESCE((SELECT SUM(lb.total_amount) FROM latest_base lb), 0) AS total_amount, - COALESCE((SELECT SUM(sbd.submitted_amount) FROM submitted_by_dealer sbd), 0) AS submitted_amount, - COALESCE((SELECT COUNT(*) FROM active_dealers), 0) AS total_dealers, - COALESCE(( - SELECT COUNT(DISTINCT sbd.dealer_code) - FROM submitted_by_dealer sbd - WHERE sbd.submitted_amount > 0 - ), 0) AS submitted_dealer_count + lb.dealer_code, + COALESCE(dm.dealer_name, lb.dealer_code) AS dealer_name, + COALESCE(dm.region_id, 'UNKNOWN') AS region_id, + lb.financial_year, + lb.quarter, + COALESCE(ls.aggregated_amount, lb.total_amount, 0)::numeric AS total_amount, + COALESCE(cbs.submitted_amount, 0)::numeric AS submitted_amount + FROM latest_base lb + LEFT JOIN dealer_meta dm ON dm.dealer_code = lb.dealer_code + LEFT JOIN latest_snapshots ls + ON ls.tan_number = lb.tan_number + AND ls.financial_year = lb.financial_year + AND ls.quarter = lb.quarter + LEFT JOIN credit_by_submission cbs ON cbs.submission_id = lb.id `, { type: QueryTypes.SELECT } ); - const totalAmount = toNum(overallRow?.total_amount); - const submittedAmount = toNum(overallRow?.submitted_amount); - const totalDealers = Math.max(0, Math.trunc(toNum(overallRow?.total_dealers))); - const submittedDealerCount = Math.max(0, Math.trunc(toNum(overallRow?.submitted_dealer_count))); + const detailRows = (detailRowsRaw || []).map((r) => ({ + dealerCode: String(r.dealer_code || '').trim(), + dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer', + regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN', + financialYear: String(r.financial_year || '').trim(), + quarter: String(r.quarter || '').trim(), + totalAmount: Math.max(0, toNum(r.total_amount)), + submittedAmount: Math.max(0, toNum(r.submitted_amount)), + })); + + const dealerAgg = new Map(); + for (const d of dealerUniverse) { + dealerAgg.set(d.dealerCode, { + dealerName: d.dealerName, + regionId: d.regionId, + totalAmount: 0, + submittedAmount: 0, + }); + } + for (const r of detailRows) { + const key = r.dealerCode; + const prev = dealerAgg.get(key) || { + dealerName: r.dealerName || key, + regionId: r.regionId || 'UNKNOWN', + totalAmount: 0, + submittedAmount: 0, + }; + prev.totalAmount += r.totalAmount; + prev.submittedAmount += r.submittedAmount; + dealerAgg.set(key, prev); + } + + const totalAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.totalAmount, 0); + const submittedAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.submittedAmount, 0); + const totalDealers = dealerAgg.size; + const submittedDealerCount = Array.from(dealerAgg.values()).filter((r) => r.submittedAmount > 0).length; const pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount); const pendingAmount = Math.max(0, totalAmount - submittedAmount); @@ -3205,153 +3622,136 @@ export async function getForm16DashboardData(): Promise { return Math.max(0, Math.min(100, Math.round((part / whole) * 100))); }; - const yearRowsRaw = await sequelize.query<{ - label: string; - total_amount: number | string | null; - dealer_count: number | string | null; - submitted_amount: number | string | null; - submitted_dealer_count: number | string | null; - }>( - ` - WITH active_dealers AS ( - SELECT DISTINCT - TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code - FROM dealers d - WHERE d.is_active = true - AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> '' - ), - latest_submissions AS ( - SELECT - s.id, - s.dealer_code, - s.financial_year, - s.quarter, - COALESCE(s.total_amount, 0)::numeric AS total_amount, - ROW_NUMBER() OVER ( - PARTITION BY s.dealer_code, s.financial_year, s.quarter - ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC - ) AS rn - FROM form16a_submissions s - INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code - ), - latest_base AS ( - SELECT id, dealer_code, financial_year, quarter, total_amount - FROM latest_submissions - WHERE rn = 1 - ), - by_year AS ( - SELECT - lb.financial_year AS label, - SUM(lb.total_amount)::numeric AS total_amount, - COUNT(DISTINCT lb.dealer_code) AS dealer_count, - SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount, - COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN lb.dealer_code END) AS submitted_dealer_count - FROM latest_base lb - LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id - GROUP BY lb.financial_year - ) - SELECT * FROM by_year - ORDER BY label DESC - `, - { type: QueryTypes.SELECT } - ); + const yearMap = new Map; submittedDealers: Set }>(); + for (const r of detailRows) { + const y = r.financialYear || 'Unknown'; + const ag = yearMap.get(y) || { totalAmount: 0, submittedAmount: 0, dealers: new Set(), submittedDealers: new Set() }; + ag.totalAmount += r.totalAmount; + ag.submittedAmount += r.submittedAmount; + ag.dealers.add(r.dealerCode); + if (r.submittedAmount > 0) ag.submittedDealers.add(r.dealerCode); + yearMap.set(y, ag); + } + const yearWise = Array.from(yearMap.entries()) + .sort((a, b) => b[0].localeCompare(a[0])) + .map(([label, ag]) => { + const dealerCount = ag.dealers.size; + const submittedDealerCount = ag.submittedDealers.size; + return { + label, + totalAmount: ag.totalAmount, + dealerCount, + submittedAmount: ag.submittedAmount, + submittedDealerCount, + pendingAmount: Math.max(0, ag.totalAmount - ag.submittedAmount), + pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount), + }; + }); - const zoneRowsRaw = await sequelize.query<{ - label: string; - total_amount: number | string | null; - dealer_count: number | string | null; - submitted_amount: number | string | null; - submitted_dealer_count: number | string | null; - }>( - ` - WITH active_dealers AS ( - SELECT DISTINCT - TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code, - CASE - WHEN UPPER(COALESCE(d.region, '')) LIKE 'N%' THEN 'North' - WHEN UPPER(COALESCE(d.region, '')) LIKE 'S%' THEN 'South' - WHEN UPPER(COALESCE(d.region, '')) LIKE 'E%' THEN 'East' - WHEN UPPER(COALESCE(d.region, '')) LIKE 'W%' THEN 'West' - WHEN UPPER(COALESCE(d.region, '')) LIKE 'C%' THEN 'Central' - ELSE 'Unknown' - END AS zone - FROM dealers d - WHERE d.is_active = true - AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> '' - ), - latest_submissions AS ( - SELECT - s.id, - s.dealer_code, - COALESCE(s.total_amount, 0)::numeric AS total_amount, - ROW_NUMBER() OVER ( - PARTITION BY s.dealer_code, s.financial_year, s.quarter - ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC - ) AS rn - FROM form16a_submissions s - INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code - ), - latest_base AS ( - SELECT id, dealer_code, total_amount - FROM latest_submissions - WHERE rn = 1 - ), - by_zone AS ( - SELECT - ad.zone AS label, - SUM(COALESCE(lb.total_amount, 0))::numeric AS total_amount, - COUNT(DISTINCT ad.dealer_code) AS dealer_count, - SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount, - COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN ad.dealer_code END) AS submitted_dealer_count - FROM active_dealers ad - LEFT JOIN latest_base lb ON lb.dealer_code = ad.dealer_code - LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id - GROUP BY ad.zone - ) - SELECT * FROM by_zone - ORDER BY CASE label - WHEN 'North' THEN 1 - WHEN 'Central' THEN 2 - WHEN 'West' THEN 3 - WHEN 'East' THEN 4 - WHEN 'South' THEN 5 - ELSE 99 - END, label - `, - { type: QueryTypes.SELECT } - ); + const dealerRegionMap = new Map(); + for (const [dealerCode, v] of dealerAgg.entries()) { + dealerRegionMap.set(dealerCode, { + dealerName: v.dealerName, + regionId: v.regionId, + totalAmount: v.totalAmount, + submittedAmount: v.submittedAmount, + }); + } - const yearWise = (yearRowsRaw || []).map((r) => { - const totalAmountRow = toNum(r.total_amount); - const submittedAmountRow = toNum(r.submitted_amount); - const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count))); - const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count))); - return { - label: r.label, - totalAmount: totalAmountRow, - dealerCount: dealerCountRow, - submittedAmount: submittedAmountRow, - submittedDealerCount: submittedDealerCountRow, - pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow), - pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow), + const zoneMap = new Map; submittedDealers: Set; regions: Map }>(); + for (const [dealerCode, d] of dealerRegionMap.entries()) { + const regionId = String(d.regionId || '').toUpperCase(); + const zoneCode = (/^[A-Z]+/.exec(regionId)?.[0] || 'UNKNOWN').toUpperCase(); + const zone = zoneMap.get(zoneCode) || { + totalAmount: 0, + submittedAmount: 0, + dealers: new Set(), + submittedDealers: new Set(), + regions: new Map(), }; - }); + zone.totalAmount += d.totalAmount; + zone.submittedAmount += d.submittedAmount; + zone.dealers.add(dealerCode); + if (d.submittedAmount > 0) zone.submittedDealers.add(dealerCode); - const zoneWise = (zoneRowsRaw || []).map((r) => { - const totalAmountRow = toNum(r.total_amount); - const submittedAmountRow = toNum(r.submitted_amount); - const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count))); - const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count))); - return { - label: r.label, - totalAmount: totalAmountRow, - dealerCount: dealerCountRow, - submittedAmount: submittedAmountRow, - submittedDealerCount: submittedDealerCountRow, - pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow), - pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow), + const regionNode = zone.regions.get(regionId) || { + regionId, + label: regionId, + totalAmount: 0, + dealerCount: 0, + submittedAmount: 0, + submittedDealerCount: 0, + pendingAmount: 0, + pendingDealerCount: 0, + dealers: [], }; - }); + regionNode.totalAmount += d.totalAmount; + regionNode.submittedAmount += d.submittedAmount; + regionNode.dealers.push({ + dealerCode, + dealerName: d.dealerName || dealerCode, + regionId, + totalAmount: d.totalAmount, + submittedAmount: d.submittedAmount, + pendingAmount: Math.max(0, d.totalAmount - d.submittedAmount), + }); + zone.regions.set(regionId, regionNode); + zoneMap.set(zoneCode, zone); + } + + const zoneHierarchy: Form16DashboardZoneNode[] = Array.from(zoneMap.entries()) + .map(([zoneCode, z]) => { + const regions = Array.from(z.regions.values()) + .map((r) => { + const submittedDealerCount = r.dealers.filter((d) => d.submittedAmount > 0).length; + const dealerCount = r.dealers.length; + const dealers = [...r.dealers].sort((a, b) => a.dealerCode.localeCompare(b.dealerCode)); + return { + ...r, + dealers, + dealerCount, + submittedDealerCount, + pendingAmount: Math.max(0, r.totalAmount - r.submittedAmount), + pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount), + }; + }) + .sort((a, b) => { + const da = regionSortValue(a.regionId); + const db = regionSortValue(b.regionId); + if (da !== db) return da - db; + return a.regionId.localeCompare(b.regionId); + }); + + const dealerCount = z.dealers.size; + const submittedDealerCount = z.submittedDealers.size; + return { + zoneCode, + label: zoneName(zoneCode), + totalAmount: z.totalAmount, + dealerCount, + submittedAmount: z.submittedAmount, + submittedDealerCount, + pendingAmount: Math.max(0, z.totalAmount - z.submittedAmount), + pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount), + regions, + }; + }) + .sort((a, b) => { + const ra = zoneSortRank(a.zoneCode); + const rb = zoneSortRank(b.zoneCode); + if (ra !== rb) return ra - rb; + return a.label.localeCompare(b.label); + }); + + const zoneWise = zoneHierarchy.map((z) => ({ + label: z.label, + totalAmount: z.totalAmount, + dealerCount: z.dealerCount, + submittedAmount: z.submittedAmount, + submittedDealerCount: z.submittedDealerCount, + pendingAmount: z.pendingAmount, + pendingDealerCount: z.pendingDealerCount, + })); return { kpi: { @@ -3370,5 +3770,6 @@ export async function getForm16DashboardData(): Promise { }, yearWise, zoneWise, + zoneHierarchy, }; }