From e081df9ced33f92e13f842960c0ec99e82406702 Mon Sep 17 00:00:00 2001 From: Joelson <200138820+JuruSysadmin@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:49:23 -0300 Subject: [PATCH] first --- package-lock.json | 233 +-- src/app.module.ts | 10 +- src/auth/auth/auth.controller.ts | 182 ++- src/auth/auth/auth.module.ts | 2 + src/auth/auth/auth.service.ts | 17 +- src/auth/auth/dto/login-audit.dto.ts | 139 ++ src/auth/services/login-audit.service.ts | 295 ++++ src/auth/services/rate-limiting.service.ts | 73 +- src/auth/services/refresh-token.service.ts | 27 +- .../services/session-management.service.ts | 17 +- src/auth/strategies/jwt-strategy.ts | 42 +- src/auth/users/UserRepository.ts | 12 + src/core/configs/cache/IRedisClient.ts | 1 + .../configs/cache/redis-client.adapter.ts | 4 + src/core/configs/typeorm.oracle.config.ts | 2 +- src/data-consult/clientes.controller.ts | 21 + src/data-consult/clientes.module.ts | 19 + src/data-consult/clientes.service.ts | 154 ++ src/data-consult/data-consult.controller.ts | 40 +- src/data-consult/data-consult.module.ts | 3 +- src/data-consult/data-consult.repository.ts | 161 +- src/data-consult/data-consult.service.ts | 79 +- src/data-consult/dto/carrier.dto.ts | 58 + src/main.ts | 9 +- src/orders/application/orders.service.ts | 239 ++- src/orders/controllers/orders.controller.ts | 282 +++- src/orders/dto/emitente.dto.ts | 27 + src/orders/dto/estlogtransfer.dto.ts | 110 ++ .../dto/find-orders-by-delivery-date.dto.ts | 113 ++ src/orders/dto/find-orders.dto.ts | 134 +- src/orders/dto/leadtime.dto.ts | 9 + src/orders/dto/mark-response.dto.ts | 21 + src/orders/dto/order-delivery.dto.ts | 23 + src/orders/dto/order-response.dto.ts | 310 ++++ .../orders-response.interceptor.ts | 20 + src/orders/interface/markdata.ts | 5 + src/orders/repositories/orders.repository.ts | 1289 ++++++++++++++--- src/partners/dto/partner.dto.ts | 16 + src/partners/partners.controller.ts | 48 + src/partners/partners.module.ts | 13 + src/partners/partners.service.ts | 168 +++ src/shared/date.util.ts | 113 ++ 42 files changed, 4129 insertions(+), 411 deletions(-) create mode 100644 src/auth/auth/dto/login-audit.dto.ts create mode 100644 src/auth/services/login-audit.service.ts create mode 100644 src/data-consult/clientes.controller.ts create mode 100644 src/data-consult/clientes.module.ts create mode 100644 src/data-consult/clientes.service.ts create mode 100644 src/data-consult/dto/carrier.dto.ts create mode 100644 src/orders/dto/emitente.dto.ts create mode 100644 src/orders/dto/estlogtransfer.dto.ts create mode 100644 src/orders/dto/find-orders-by-delivery-date.dto.ts create mode 100644 src/orders/dto/leadtime.dto.ts create mode 100644 src/orders/dto/mark-response.dto.ts create mode 100644 src/orders/dto/order-delivery.dto.ts create mode 100644 src/orders/dto/order-response.dto.ts create mode 100644 src/orders/interceptors/orders-response.interceptor.ts create mode 100644 src/orders/interface/markdata.ts create mode 100644 src/partners/dto/partner.dto.ts create mode 100644 src/partners/partners.controller.ts create mode 100644 src/partners/partners.module.ts create mode 100644 src/partners/partners.service.ts create mode 100644 src/shared/date.util.ts diff --git a/package-lock.json b/package-lock.json index 84b3b07..f3642aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -745,6 +745,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", + "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.6.tgz", @@ -793,15 +803,15 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.11.tgz", - "integrity": "sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", + "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.0", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -821,15 +831,15 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.11.tgz", - "integrity": "sha512-YoZr0lBnnLFPpfPSNsQ8IZyKxU47zPyVi9NLjCWtna52//M/xuL0PGPAxHxxYhdOhnvY2oBafoM+BI5w/JK7jw==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz", + "integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.2.2", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -866,10 +876,49 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", "dev": true, "license": "MIT", "engines": { @@ -1046,9 +1095,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", - "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "dev": true, "license": "MIT", "engines": { @@ -3485,13 +3534,13 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -3822,9 +3871,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4130,9 +4179,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true, "license": "MIT" }, @@ -5480,34 +5529,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -5776,14 +5797,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -6051,9 +6073,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7613,15 +7635,16 @@ } }, "node_modules/jsdom/node_modules/form-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", - "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35" }, "engines": { @@ -8687,16 +8710,6 @@ "node": ">=14.6" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -10312,16 +10325,23 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -11347,19 +11367,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11367,6 +11374,26 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -11767,6 +11794,20 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -11890,9 +11931,9 @@ } }, "node_modules/typeorm/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" diff --git a/src/app.module.ts b/src/app.module.ts index 08e3b87..6caa18d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,8 @@ import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler'; import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware'; import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware'; import { HealthModule } from './health/health.module'; +import { clientes } from './data-consult/clientes.module'; +import { PartnersModule } from './partners/partners.module'; @Module({ @@ -30,7 +32,7 @@ import { HealthModule } from './health/health.module'; UsersModule, ConfigModule.forRoot({ isGlobal: true, load: [jwtConfig] - }), + }), TypeOrmModule.forRootAsync({ name: 'oracle', inject: [ConfigService], @@ -57,6 +59,7 @@ import { HealthModule } from './health/health.module'; OrdersPaymentModule, HttpModule, OrdersModule, + clientes, ProductsModule, NegotiationsModule, OccurrencesModule, @@ -66,16 +69,17 @@ import { HealthModule } from './health/health.module'; AuthModule, OrdersModule, HealthModule, + PartnersModule, ], controllers: [OcorrencesController, LogisticController ], - providers: [ LogisticService, ], + providers: [ LogisticService,], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(RequestSanitizerMiddleware) .forRoutes('*'); - + consumer .apply(RateLimiterMiddleware) .forRoutes('auth', 'users'); diff --git a/src/auth/auth/auth.controller.ts b/src/auth/auth/auth.controller.ts index e27be03..dc89199 100644 --- a/src/auth/auth/auth.controller.ts +++ b/src/auth/auth/auth.controller.ts @@ -9,6 +9,7 @@ import { UseGuards, Request, Param, + Query, } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs'; @@ -16,6 +17,7 @@ import { AuthenticateUserCommand } from './commands/authenticate-user.command'; import { LoginResponseDto } from './dto/LoginResponseDto'; import { LoginDto } from './dto/login.dto'; import { ResultModel } from 'src/core/models/result.model'; +import { DateUtil } from 'src/shared/date.util'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RateLimitingGuard } from '../guards/rate-limiting.guard'; @@ -24,6 +26,13 @@ import { RefreshTokenService } from '../services/refresh-token.service'; import { SessionManagementService } from '../services/session-management.service'; import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto'; import { SessionsResponseDto } from './dto/session.dto'; +import { LoginAuditService } from '../services/login-audit.service'; +import { + LoginAuditFiltersDto, + LoginAuditResponseDto, + LoginStatsDto, + LoginStatsFiltersDto +} from './dto/login-audit.dto'; import { ApiTags, ApiOperation, @@ -44,6 +53,7 @@ export class AuthController { private readonly rateLimitingService: RateLimitingService, private readonly refreshTokenService: RefreshTokenService, private readonly sessionManagementService: SessionManagementService, + private readonly loginAuditService: LoginAuditService, ) {} @Post('login') @@ -62,30 +72,47 @@ export class AuthController { const command = new AuthenticateUserCommand(dto.username, dto.password); const result = await this.commandBus.execute(command); + const userAgent = req.headers['user-agent'] || 'Unknown'; + if (!result.success) { - // Registra tentativa falhada await this.rateLimitingService.recordAttempt(ip, false); + await this.loginAuditService.logLoginAttempt({ + username: dto.username, + ipAddress: ip, + userAgent, + success: false, + failureReason: result.error, + }); + throw new HttpException( new ResultModel(false, result.error, null, result.error), HttpStatus.UNAUTHORIZED, ); } - // Registra tentativa bem-sucedida (limpa contador) await this.rateLimitingService.recordAttempt(ip, true); const user = result.data; - const userAgent = req.headers['user-agent'] || 'Unknown'; - // Cria sessão para o usuário primeiro + /** + * Verifica se o usuário já possui uma sessão ativa + */ + const existingSession = await this.sessionManagementService.hasActiveSession(user.id); + + if (existingSession) { + /** + * Encerra a sessão existente antes de criar uma nova + */ + await this.sessionManagementService.terminateSession(user.id, existingSession.sessionId); + } + const session = await this.sessionManagementService.createSession( user.id, ip, userAgent, ); - // Cria tokens de acesso e refresh com sessionId const tokenPair = await this.authService.createTokenPair( user.id, user.sellerId, @@ -95,11 +122,20 @@ export class AuthController { session.sessionId, ); + await this.loginAuditService.logLoginAttempt({ + userId: user.id, + username: dto.username, + ipAddress: ip, + userAgent, + success: true, + sessionId: session.sessionId, + }); + return { id: user.id, sellerId: user.sellerId, name: user.name, - username: user.name, + username: dto.username, storeId: user.storeId, email: user.email, accessToken: tokenPair.accessToken, @@ -173,7 +209,7 @@ export class AuthController { @ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' }) async getSessions(@Request() req): Promise { const userId = req.user.id; - const currentSessionId = req.user.sessionId; // ID da sessão atual + const currentSessionId = req.user.sessionId; const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId); return { @@ -181,8 +217,8 @@ export class AuthController { sessionId: session.sessionId, ipAddress: session.ipAddress, userAgent: session.userAgent, - createdAt: new Date(session.createdAt).toISOString(), - lastActivity: new Date(session.lastActivity).toISOString(), + createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)), + lastActivity: DateUtil.toBrazilISOString(new Date(session.lastActivity)), isCurrent: session.sessionId === currentSessionId, })), total: sessions.length, @@ -222,4 +258,132 @@ export class AuthController { message: 'Todas as sessões foram encerradas com sucesso', }; } + + @Get('audit/logs') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Consulta logs de auditoria de login' }) + @ApiOkResponse({ + description: 'Lista de logs de auditoria', + type: LoginAuditResponseDto, + }) + @ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' }) + async getLoginAuditLogs( + @Query() filters: LoginAuditFiltersDto, + @Request() req, + ): Promise { + const userId = req.user.id; + + const auditFilters = { + ...filters, + userId: filters.userId || userId, + startDate: filters.startDate ? new Date(filters.startDate) : undefined, + endDate: filters.endDate ? new Date(filters.endDate) : undefined, + }; + + const logs = await this.loginAuditService.getLoginLogs(auditFilters); + + return { + logs: logs.map(log => ({ + ...log, + timestamp: DateUtil.toBrazilISOString(log.timestamp), + })), + total: logs.length, + page: Math.floor((filters.offset || 0) / (filters.limit || 100)) + 1, + limit: filters.limit || 100, + }; + } + + @Get('audit/stats') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Obtém estatísticas de login' }) + @ApiOkResponse({ + description: 'Estatísticas de login', + type: LoginStatsDto, + }) + @ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' }) + async getLoginStats( + @Query() filters: LoginStatsFiltersDto, + @Request() req, + ): Promise { + const userId = req.user.id; + const days = filters.days || 7; + + const stats = await this.loginAuditService.getLoginStats( + filters.userId || userId, + days, + ); + + return stats; + } + + @Get('session/status') + @ApiOperation({ summary: 'Verifica se o usuário possui uma sessão ativa' }) + @ApiOkResponse({ + description: 'Status da sessão do usuário', + schema: { + type: 'object', + properties: { + hasActiveSession: { type: 'boolean' }, + sessionInfo: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + ipAddress: { type: 'string' }, + userAgent: { type: 'string' }, + createdAt: { type: 'string' }, + lastActivity: { type: 'string' } + } + } + } + } + }) + async checkSessionStatus(@Query('username') username: string): Promise<{ + hasActiveSession: boolean; + sessionInfo?: { + sessionId: string; + ipAddress: string; + userAgent: string; + createdAt: string; + lastActivity: string; + }; + }> { + if (!username) { + throw new HttpException( + new ResultModel(false, 'Username é obrigatório', null, 'Username é obrigatório'), + HttpStatus.BAD_REQUEST, + ); + } + + /** + * Busca o usuário pelo username para obter o ID + */ + const user = await this.authService.findUserByUsername(username); + + if (!user) { + return { + hasActiveSession: false, + }; + } + + const activeSession = await this.sessionManagementService.hasActiveSession(user.id); + + if (!activeSession) { + return { + hasActiveSession: false, + }; + } + + return { + hasActiveSession: true, + sessionInfo: { + sessionId: activeSession.sessionId, + ipAddress: activeSession.ipAddress, + userAgent: activeSession.userAgent, + createdAt: DateUtil.toBrazilISOString(new Date(activeSession.createdAt)), + lastActivity: DateUtil.toBrazilISOString(new Date(activeSession.lastActivity)), + }, + }; + } } diff --git a/src/auth/auth/auth.module.ts b/src/auth/auth/auth.module.ts index c32b187..2851a01 100644 --- a/src/auth/auth/auth.module.ts +++ b/src/auth/auth/auth.module.ts @@ -13,6 +13,7 @@ import { TokenBlacklistService } from '../services/token-blacklist.service'; import { RateLimitingService } from '../services/rate-limiting.service'; import { RefreshTokenService } from '../services/refresh-token.service'; import { SessionManagementService } from '../services/session-management.service'; +import { LoginAuditService } from '../services/login-audit.service'; @Module({ imports: [ @@ -40,6 +41,7 @@ import { SessionManagementService } from '../services/session-management.service RateLimitingService, RefreshTokenService, SessionManagementService, + LoginAuditService, AuthenticateUserHandler ], exports: [AuthService], diff --git a/src/auth/auth/auth.service.ts b/src/auth/auth/auth.service.ts index 766370c..9a4dea6 100644 --- a/src/auth/auth/auth.service.ts +++ b/src/auth/auth/auth.service.ts @@ -37,11 +37,12 @@ export class AuthService { * @param username Nome de usuário * @param email Email do usuário * @param storeId ID da loja + * @param sessionId ID da sessão * @returns Objeto com access token e refresh token */ async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) { const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId); - const refreshToken = await this.refreshTokenService.generateRefreshToken(id); + const refreshToken = await this.refreshTokenService.generateRefreshToken(id, sessionId); return { accessToken, @@ -68,7 +69,8 @@ export class AuthService { user.sellerId, user.name, user.email, - user.storeId + user.storeId, + tokenData.sessionId ); return { @@ -85,7 +87,7 @@ export class AuthService { id: user.id, sellerId: user.sellerId, storeId: user.storeId, - username: user.name, // Usando name como username para consistência + username: user.name, email: user.email, }; } @@ -106,4 +108,13 @@ export class AuthService { async isTokenBlacklisted(token: string): Promise { return this.tokenBlacklistService.isBlacklisted(token); } + + /** + * Busca um usuário pelo username + * @param username Nome de usuário + * @returns Dados do usuário se encontrado + */ + async findUserByUsername(username: string) { + return this.userRepository.findByUsername(username); + } } \ No newline at end of file diff --git a/src/auth/auth/dto/login-audit.dto.ts b/src/auth/auth/dto/login-audit.dto.ts new file mode 100644 index 0000000..40c2c36 --- /dev/null +++ b/src/auth/auth/dto/login-audit.dto.ts @@ -0,0 +1,139 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsNumber, IsString, IsBoolean, IsDateString, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class LoginAuditFiltersDto { + @ApiProperty({ description: 'ID do usuário', required: false }) + @IsOptional() + @IsNumber() + @Type(() => Number) + userId?: number; + + @ApiProperty({ description: 'Nome de usuário', required: false }) + @IsOptional() + @IsString() + username?: string; + + @ApiProperty({ description: 'Endereço IP', required: false }) + @IsOptional() + @IsString() + ipAddress?: string; + + @ApiProperty({ description: 'Filtrar apenas logins bem-sucedidos', required: false }) + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + success?: boolean; + + @ApiProperty({ description: 'Data de início (ISO string)', required: false }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiProperty({ description: 'Data de fim (ISO string)', required: false }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiProperty({ description: 'Número de registros por página', required: false, minimum: 1, maximum: 1000 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(1) + @Max(1000) + limit?: number; + + @ApiProperty({ description: 'Offset para paginação', required: false, minimum: 0 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(0) + offset?: number; +} + +export class LoginAuditLogDto { + @ApiProperty({ description: 'ID único do log' }) + id: string; + + @ApiProperty({ description: 'ID do usuário', required: false }) + userId?: number; + + @ApiProperty({ description: 'Nome de usuário' }) + username: string; + + @ApiProperty({ description: 'Endereço IP' }) + ipAddress: string; + + @ApiProperty({ description: 'User Agent' }) + userAgent: string; + + @ApiProperty({ description: 'Login bem-sucedido' }) + success: boolean; + + @ApiProperty({ description: 'Motivo da falha', required: false }) + failureReason?: string; + + @ApiProperty({ description: 'Timestamp do login' }) + timestamp: string; + + @ApiProperty({ description: 'ID da sessão', required: false }) + sessionId?: string; + + @ApiProperty({ description: 'Localização estimada', required: false }) + location?: string; +} + +export class LoginAuditResponseDto { + @ApiProperty({ description: 'Lista de logs de login', type: [LoginAuditLogDto] }) + logs: LoginAuditLogDto[]; + + @ApiProperty({ description: 'Total de registros encontrados' }) + total: number; + + @ApiProperty({ description: 'Página atual' }) + page: number; + + @ApiProperty({ description: 'Registros por página' }) + limit: number; +} + +export class LoginStatsDto { + @ApiProperty({ description: 'Total de tentativas' }) + totalAttempts: number; + + @ApiProperty({ description: 'Logins bem-sucedidos' }) + successfulLogins: number; + + @ApiProperty({ description: 'Logins falhados' }) + failedLogins: number; + + @ApiProperty({ description: 'Número de IPs únicos' }) + uniqueIps: number; + + @ApiProperty({ description: 'IPs mais frequentes' }) + topIps: Array<{ ip: string; count: number }>; + + @ApiProperty({ description: 'Estatísticas diárias' }) + dailyStats: Array<{ + date: string; + attempts: number; + successes: number; + failures: number; + }>; +} + +export class LoginStatsFiltersDto { + @ApiProperty({ description: 'ID do usuário para estatísticas', required: false }) + @IsOptional() + @IsNumber() + @Type(() => Number) + userId?: number; + + @ApiProperty({ description: 'Número de dias para análise', required: false, minimum: 1, maximum: 365 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(1) + @Max(365) + days?: number; +} diff --git a/src/auth/services/login-audit.service.ts b/src/auth/services/login-audit.service.ts new file mode 100644 index 0000000..5b6449d --- /dev/null +++ b/src/auth/services/login-audit.service.ts @@ -0,0 +1,295 @@ +import { Injectable, Inject } from '@nestjs/common'; +import Redis from 'ioredis'; +import { DateUtil } from 'src/shared/date.util'; + +export interface LoginAuditLog { + id: string; + userId?: number; + username: string; + ipAddress: string; + userAgent: string; + success: boolean; + failureReason?: string; + timestamp: Date; + sessionId?: string; + location?: string; +} + +export interface LoginAuditFilters { + userId?: number; + username?: string; + ipAddress?: string; + success?: boolean; + startDate?: Date; + endDate?: Date; + limit?: number; + offset?: number; +} + +@Injectable() +export class LoginAuditService { + private readonly LOG_PREFIX = 'login_audit'; + private readonly LOG_EXPIRY = 30 * 24 * 60 * 60; // 30 dias em segundos + + constructor( + @Inject('REDIS_CLIENT') private readonly redis: Redis, + ) {} + + /** + * Registra uma tentativa de login + * @param log Dados do log de login + */ + async logLoginAttempt(log: Omit): Promise { + const logId = this.generateLogId(); + const timestamp = DateUtil.now(); + + const auditLog: LoginAuditLog = { + ...log, + id: logId, + timestamp, + }; + + const logKey = this.buildLogKey(logId); + await this.redis.setex(logKey, this.LOG_EXPIRY, JSON.stringify(auditLog)); + + if (log.userId) { + const userLogsKey = this.buildUserLogsKey(log.userId); + await this.redis.lpush(userLogsKey, logId); + await this.redis.expire(userLogsKey, this.LOG_EXPIRY); + } + + const ipLogsKey = this.buildIpLogsKey(log.ipAddress); + await this.redis.lpush(ipLogsKey, logId); + await this.redis.expire(ipLogsKey, this.LOG_EXPIRY); + + const globalLogsKey = this.buildGlobalLogsKey(); + await this.redis.lpush(globalLogsKey, logId); + await this.redis.ltrim(globalLogsKey, 0, 999); + await this.redis.expire(globalLogsKey, this.LOG_EXPIRY); + + const dateKey = DateUtil.toBrazilString(timestamp, 'yyyy-MM-dd'); + const dateLogsKey = this.buildDateLogsKey(dateKey); + await this.redis.lpush(dateLogsKey, logId); + await this.redis.expire(dateLogsKey, this.LOG_EXPIRY); + } + + /** + * Busca logs de login com filtros + * @param filters Filtros para a busca + * @returns Lista de logs de login + */ + async getLoginLogs(filters: LoginAuditFilters = {}): Promise { + let logIds: string[] = []; + + if (filters.userId) { + const userLogsKey = this.buildUserLogsKey(filters.userId); + logIds = await this.redis.lrange(userLogsKey, 0, -1); + } else if (filters.ipAddress) { + const ipLogsKey = this.buildIpLogsKey(filters.ipAddress); + logIds = await this.redis.lrange(ipLogsKey, 0, -1); + } else if (filters.startDate || filters.endDate) { + const startDate = filters.startDate || new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000); + const endDate = filters.endDate || DateUtil.now(); + + const dates = this.getDateRange(startDate, endDate); + for (const date of dates) { + const dateLogsKey = this.buildDateLogsKey(date); + const dateLogIds = await this.redis.lrange(dateLogsKey, 0, -1); + logIds.push(...dateLogIds); + } + } else { + const globalLogsKey = this.buildGlobalLogsKey(); + logIds = await this.redis.lrange(globalLogsKey, 0, -1); + } + + const logs: LoginAuditLog[] = []; + for (const logId of logIds) { + const logKey = this.buildLogKey(logId); + const logData = await this.redis.get(logKey); + + if (logData) { + const log: LoginAuditLog = JSON.parse(logData as string); + + /** + * Converte timestamp de string para Date se necessário + */ + if (typeof log.timestamp === 'string') { + log.timestamp = new Date(log.timestamp); + } + + if (this.matchesFilters(log, filters)) { + logs.push(log); + } + } + } + + logs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + + const offset = filters.offset || 0; + const limit = filters.limit || 100; + + return logs.slice(offset, offset + limit); + } + + /** + * Busca estatísticas de login + * @param userId ID do usuário (opcional) + * @param days Número de dias para análise (padrão: 7) + * @returns Estatísticas de login + */ + async getLoginStats(userId?: number, days: number = 7): Promise<{ + totalAttempts: number; + successfulLogins: number; + failedLogins: number; + uniqueIps: number; + topIps: Array<{ ip: string; count: number }>; + dailyStats: Array<{ date: string; attempts: number; successes: number; failures: number }>; + }> { + const endDate = DateUtil.now(); + const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000); + + const filters: LoginAuditFilters = { + startDate, + endDate, + limit: 10000, // Limite alto para estatísticas + }; + + if (userId) { + filters.userId = userId; + } + + const logs = await this.getLoginLogs(filters); + + const stats = { + totalAttempts: logs.length, + successfulLogins: logs.filter(log => log.success).length, + failedLogins: logs.filter(log => !log.success).length, + uniqueIps: new Set(logs.map(log => log.ipAddress)).size, + topIps: [] as Array<{ ip: string; count: number }>, + dailyStats: [] as Array<{ date: string; attempts: number; successes: number; failures: number }>, + }; + + const ipCounts = new Map(); + logs.forEach(log => { + ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1); + }); + + stats.topIps = Array.from(ipCounts.entries()) + .map(([ip, count]) => ({ ip, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + const dailyCounts = new Map(); + logs.forEach(log => { + const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd'); + const dayStats = dailyCounts.get(date) || { attempts: 0, successes: 0, failures: 0 }; + dayStats.attempts++; + if (log.success) { + dayStats.successes++; + } else { + dayStats.failures++; + } + dailyCounts.set(date, dayStats); + }); + + stats.dailyStats = Array.from(dailyCounts.entries()) + .map(([date, counts]) => ({ date, ...counts })) + .sort((a, b) => a.date.localeCompare(b.date)); + + return stats; + } + + /** + * Remove logs antigos (mais de 30 dias) + */ + async cleanupOldLogs(): Promise { + const cutoffDate = new Date(DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000); + const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd'); + + const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate); + for (const date of oldDates) { + const dateLogsKey = this.buildDateLogsKey(date); + await this.redis.del(dateLogsKey); + } + } + + /** + * Gera um ID único para o log + */ + private generateLogId(): string { + return `${DateUtil.nowTimestamp()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * Constrói a chave para um log específico + */ + private buildLogKey(logId: string): string { + return `${this.LOG_PREFIX}:log:${logId}`; + } + + /** + * Constrói a chave para logs de um usuário + */ + private buildUserLogsKey(userId: number): string { + return `${this.LOG_PREFIX}:user:${userId}`; + } + + /** + * Constrói a chave para logs de um IP + */ + private buildIpLogsKey(ipAddress: string): string { + return `${this.LOG_PREFIX}:ip:${ipAddress}`; + } + + /** + * Constrói a chave para logs globais + */ + private buildGlobalLogsKey(): string { + return `${this.LOG_PREFIX}:global`; + } + + /** + * Constrói a chave para logs de uma data específica + */ + private buildDateLogsKey(date: string): string { + return `${this.LOG_PREFIX}:date:${date}`; + } + + /** + * Verifica se um log corresponde aos filtros + */ + private matchesFilters(log: LoginAuditLog, filters: LoginAuditFilters): boolean { + if (filters.username && !log.username.toLowerCase().includes(filters.username.toLowerCase())) { + return false; + } + + if (filters.success !== undefined && log.success !== filters.success) { + return false; + } + + if (filters.startDate && log.timestamp < filters.startDate) { + return false; + } + + if (filters.endDate && log.timestamp > filters.endDate) { + return false; + } + + return true; + } + + /** + * Gera array de datas entre startDate e endDate + */ + private getDateRange(startDate: Date, endDate: Date): string[] { + const dates: string[] = []; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + dates.push(DateUtil.toBrazilString(currentDate, 'yyyy-MM-dd')); + currentDate.setDate(currentDate.getDate() + 1); + } + + return dates; + } +} diff --git a/src/auth/services/rate-limiting.service.ts b/src/auth/services/rate-limiting.service.ts index 4ce1bf5..1918430 100644 --- a/src/auth/services/rate-limiting.service.ts +++ b/src/auth/services/rate-limiting.service.ts @@ -21,7 +21,7 @@ export class RateLimitingService { ) {} /** - * Verifica se o IP pode fazer uma tentativa de login + * Verifica se o IP pode fazer uma tentativa de login usando operações atômicas * @param ip Endereço IP do cliente * @param config Configuração personalizada (opcional) * @returns true se permitido, false se bloqueado @@ -31,23 +31,51 @@ export class RateLimitingService { const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); - // Verifica se está bloqueado - const isBlocked = await this.redis.get(blockKey); - if (isBlocked) { - return false; - } + /** + * Usa script Lua para operação atômica (verificação e incremento em uma única operação) + */ + const luaScript = ` + local key = KEYS[1] + local blockKey = KEYS[2] + local maxAttempts = tonumber(ARGV[1]) + local windowMs = tonumber(ARGV[2]) + local blockDurationMs = tonumber(ARGV[3]) + + -- Verifica se já está bloqueado + local isBlocked = redis.call('GET', blockKey) + if isBlocked then + return {0, 1} -- attempts=0, blocked=1 + end + + -- Incrementa contador de tentativas + local attempts = redis.call('INCR', key) + + -- Se é a primeira tentativa, define TTL + if attempts == 1 then + redis.call('EXPIRE', key, windowMs / 1000) + end + + -- Se excedeu limite, bloqueia + if attempts > maxAttempts then + redis.call('SET', blockKey, 'blocked', 'EX', blockDurationMs / 1000) + return {attempts, 1} -- attempts, blocked=1 + end + + return {attempts, 0} -- attempts, blocked=0 + `; - // Conta tentativas na janela de tempo - const attempts = await this.redis.get(key); - const attemptCount = attempts ? parseInt(attempts) : 0; + const result = await this.redis.eval( + luaScript, + 2, + key, + blockKey, + finalConfig.maxAttempts, + finalConfig.windowMs, + finalConfig.blockDurationMs + ) as [number, number]; - if (attemptCount >= finalConfig.maxAttempts) { - // Bloqueia o IP - await this.redis.set(blockKey, 'blocked', finalConfig.blockDurationMs / 1000); - return false; - } - - return true; + const [attempts, isBlockedResult] = result; + return isBlockedResult === 0; } /** @@ -59,15 +87,18 @@ export class RateLimitingService { async recordAttempt(ip: string, success: boolean, config?: Partial): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const key = this.buildAttemptKey(ip); + const blockKey = this.buildBlockKey(ip); if (success) { + /** + * Limpa tentativas e bloqueio em caso de sucesso + */ await this.redis.del(key); - } else { - const attempts = await this.redis.get(key); - const attemptCount = attempts ? parseInt(attempts) + 1 : 1; - - await this.redis.set(key, attemptCount.toString(), finalConfig.windowMs / 1000); + await this.redis.del(blockKey); } + /** + * Para falhas, o incremento já foi feito no isAllowed() de forma atômica + */ } /** diff --git a/src/auth/services/refresh-token.service.ts b/src/auth/services/refresh-token.service.ts index d063e5e..9a412f5 100644 --- a/src/auth/services/refresh-token.service.ts +++ b/src/auth/services/refresh-token.service.ts @@ -4,10 +4,12 @@ import { IRedisClient } from '../../core/configs/cache/IRedisClient'; import { JwtService } from '@nestjs/jwt'; import { JwtPayload } from '../models/jwt-payload.model'; import { randomBytes } from 'crypto'; +import { DateUtil } from 'src/shared/date.util'; export interface RefreshTokenData { userId: number; tokenId: string; + sessionId?: string; expiresAt: number; createdAt: number; } @@ -25,26 +27,30 @@ export class RefreshTokenService { /** * Gera um novo refresh token para o usuário * @param userId ID do usuário + * @param sessionId ID da sessão (opcional) * @returns Refresh token */ - async generateRefreshToken(userId: number): Promise { + async generateRefreshToken(userId: number, sessionId?: string): Promise { const tokenId = randomBytes(32).toString('hex'); const refreshToken = this.jwtService.sign( - { userId, tokenId, type: 'refresh' }, + { userId, tokenId, sessionId, type: 'refresh' }, { expiresIn: '7d' } ); const tokenData: RefreshTokenData = { userId, tokenId, - expiresAt: Date.now() + (this.REFRESH_TOKEN_TTL * 1000), - createdAt: Date.now(), + sessionId, + expiresAt: DateUtil.nowTimestamp() + (this.REFRESH_TOKEN_TTL * 1000), + createdAt: DateUtil.nowTimestamp(), }; const key = this.buildRefreshTokenKey(userId, tokenId); await this.redis.set(key, tokenData, this.REFRESH_TOKEN_TTL); - // Limita o número de refresh tokens por usuário + /** + * Limita o número de refresh tokens por usuário + */ await this.limitRefreshTokensPerUser(userId); return refreshToken; @@ -63,7 +69,7 @@ export class RefreshTokenService { throw new UnauthorizedException('Token inválido'); } - const { userId, tokenId } = decoded; + const { userId, tokenId, sessionId } = decoded; const key = this.buildRefreshTokenKey(userId, tokenId); const tokenData = await this.redis.get(key); @@ -71,7 +77,7 @@ export class RefreshTokenService { throw new UnauthorizedException('Refresh token expirado ou inválido'); } - if (tokenData.expiresAt < Date.now()) { + if (tokenData.expiresAt < DateUtil.nowTimestamp()) { await this.revokeRefreshToken(userId, tokenId); throw new UnauthorizedException('Refresh token expirado'); } @@ -82,6 +88,7 @@ export class RefreshTokenService { storeId: '', username: '', email: '', + sessionId: sessionId || tokenData.sessionId, tokenId } as JwtPayload; } catch (error) { @@ -125,7 +132,7 @@ export class RefreshTokenService { for (const key of keys) { const tokenData = await this.redis.get(key); - if (tokenData && tokenData.expiresAt > Date.now()) { + if (tokenData && tokenData.expiresAt > DateUtil.nowTimestamp()) { tokens.push(tokenData); } } @@ -141,7 +148,9 @@ export class RefreshTokenService { const activeTokens = await this.getActiveRefreshTokens(userId); if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) { - // Remove os tokens mais antigos + /** + * Remove os tokens mais antigos + */ const tokensToRemove = activeTokens .slice(this.MAX_REFRESH_TOKENS_PER_USER) .map(token => token.tokenId); diff --git a/src/auth/services/session-management.service.ts b/src/auth/services/session-management.service.ts index d050a73..e3fdc21 100644 --- a/src/auth/services/session-management.service.ts +++ b/src/auth/services/session-management.service.ts @@ -2,6 +2,7 @@ import { Injectable, Inject, NotFoundException } from '@nestjs/common'; import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; import { IRedisClient } from '../../core/configs/cache/IRedisClient'; import { randomBytes } from 'crypto'; +import { DateUtil } from 'src/shared/date.util'; export interface SessionData { sessionId: string; @@ -16,7 +17,7 @@ export interface SessionData { @Injectable() export class SessionManagementService { private readonly SESSION_TTL = 8 * 60 * 60; // 8 horas em segundos - private readonly MAX_SESSIONS_PER_USER = 5; // Máximo 5 sessões por usuário + private readonly MAX_SESSIONS_PER_USER = 1; // Máximo 1 sessão por usuário constructor( @Inject(RedisClientToken) private readonly redis: IRedisClient, @@ -31,7 +32,7 @@ export class SessionManagementService { */ async createSession(userId: number, ipAddress: string, userAgent: string): Promise { const sessionId = randomBytes(16).toString('hex'); - const now = Date.now(); + const now = DateUtil.nowTimestamp(); const sessionData: SessionData = { sessionId, @@ -62,7 +63,7 @@ export class SessionManagementService { const sessionData = await this.redis.get(key); if (sessionData) { - sessionData.lastActivity = Date.now(); + sessionData.lastActivity = DateUtil.nowTimestamp(); await this.redis.set(key, sessionData, this.SESSION_TTL); } } @@ -158,6 +159,16 @@ export class SessionManagementService { return sessionData ? sessionData.isActive : false; } + /** + * Verifica se o usuário possui uma sessão ativa + * @param userId ID do usuário + * @returns Dados da sessão ativa se existir, null caso contrário + */ + async hasActiveSession(userId: number): Promise { + const activeSessions = await this.getActiveSessions(userId); + return activeSessions.length > 0 ? activeSessions[0] : null; + } + /** * Limita o número de sessões por usuário * @param userId ID do usuário diff --git a/src/auth/strategies/jwt-strategy.ts b/src/auth/strategies/jwt-strategy.ts index 5c36936..b72c8f0 100644 --- a/src/auth/strategies/jwt-strategy.ts +++ b/src/auth/strategies/jwt-strategy.ts @@ -31,17 +31,34 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException('Token foi invalidado'); } - const sessionKey = this.buildSessionKey(payload.id); + /** + * Usa a mesma chave que o SessionManagementService + * Formato: auth:sessions:userId:sessionId + */ + const sessionKey = this.buildSessionKey(payload.id, payload.sessionId); const cachedUser = await this.redis.get(sessionKey); if (cachedUser) { + /** + * Verifica se a sessão ainda está ativa + */ + const isSessionActive = await this.sessionManagementService.isSessionActive( + payload.id, + payload.sessionId + ); + + if (!isSessionActive) { + throw new UnauthorizedException('Sessão expirada ou inválida'); + } + return { id: cachedUser.id, sellerId: cachedUser.sellerId, storeId: cachedUser.storeId, - username: cachedUser.name, + username: cachedUser.username, // ← Corrigido: usar username em vez de name email: cachedUser.email, name: cachedUser.name, + sessionId: payload.sessionId, }; } @@ -50,14 +67,21 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException('Usuário inválido ou inativo'); } + /** + * Verifica se usuário está bloqueado (consistência com AuthenticateUserHandler) + */ + if (user.situacao === 'B') { + throw new UnauthorizedException('Usuário bloqueado, acesso não permitido'); + } + const userData = { id: user.id, sellerId: user.sellerId, storeId: user.storeId, - username: user.name, + username: user.name, // ← Manter name como username para compatibilidade email: user.email, name: user.name, - sessionId: payload.sessionId, // Inclui sessionId do token + sessionId: payload.sessionId, }; await this.redis.set(sessionKey, userData, 60 * 60 * 8); @@ -65,7 +89,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return userData; } - private buildSessionKey(userId: number): string { - return `auth:sessions:${userId}`; + /** + * Constrói a chave de sessão no mesmo formato do SessionManagementService + * @param userId ID do usuário + * @param sessionId ID da sessão + * @returns Chave para o Redis + */ + private buildSessionKey(userId: number, sessionId: string): string { + return `auth:sessions:${userId}:${sessionId}`; } } diff --git a/src/auth/users/UserRepository.ts b/src/auth/users/UserRepository.ts index c6d93b9..08e647b 100644 --- a/src/auth/users/UserRepository.ts +++ b/src/auth/users/UserRepository.ts @@ -73,4 +73,16 @@ export class UserRepository { const result = await this.dataSource.query(sql, [id]); return result[0] || null; } + + async findByUsername(username: string) { + const sql = ` + SELECT MATRICULA AS "id", NOME AS "name", CODUSUR AS "sellerId", + CODFILIAL AS "storeId", EMAIL AS "email", + DTDEMISSAO as "dataDesligamento", SITUACAO as "situacao" + FROM PCEMPR + WHERE USUARIOBD = :1 + `; + const result = await this.dataSource.query(sql, [username.toUpperCase()]); + return result[0] || null; + } } diff --git a/src/core/configs/cache/IRedisClient.ts b/src/core/configs/cache/IRedisClient.ts index 5dc0a99..cc801fe 100644 --- a/src/core/configs/cache/IRedisClient.ts +++ b/src/core/configs/cache/IRedisClient.ts @@ -5,5 +5,6 @@ export interface IRedisClient { del(...keys: string[]): Promise; keys(pattern: string): Promise; ttl(key: string): Promise; + eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise; } \ No newline at end of file diff --git a/src/core/configs/cache/redis-client.adapter.ts b/src/core/configs/cache/redis-client.adapter.ts index ddfdd51..e10d386 100644 --- a/src/core/configs/cache/redis-client.adapter.ts +++ b/src/core/configs/cache/redis-client.adapter.ts @@ -35,4 +35,8 @@ export class RedisClientAdapter implements IRedisClient { async ttl(key: string): Promise { return this.redis.ttl(key); } + + async eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise { + return this.redis.eval(script, numKeys, ...keysAndArgs); + } } diff --git a/src/core/configs/typeorm.oracle.config.ts b/src/core/configs/typeorm.oracle.config.ts index d9e1dc0..e6bf74d 100644 --- a/src/core/configs/typeorm.oracle.config.ts +++ b/src/core/configs/typeorm.oracle.config.ts @@ -33,7 +33,7 @@ export function createOracleConfig(config: ConfigService): DataSourceOptions { username: config.get('ORACLE_USER'), password: config.get('ORACLE_PASSWORD'), synchronize: false, - logging: config.get('NODE_ENV') === 'development', + logging: false, entities: [__dirname + '/../**/*.entity.{ts,js}'], extra: { poolMin: validPoolMin, diff --git a/src/data-consult/clientes.controller.ts b/src/data-consult/clientes.controller.ts new file mode 100644 index 0000000..cc76001 --- /dev/null +++ b/src/data-consult/clientes.controller.ts @@ -0,0 +1,21 @@ + +import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { Controller, Get, Param } from '@nestjs/common'; +import { clientesService } from './clientes.service'; + +@ApiTags('clientes') +@Controller('api/v1/') +export class clientesController { + + constructor(private readonly clientesService: clientesService) {} + + + @Get('clientes/:filter') + async customer(@Param('filter') filter: string) { + return this.clientesService.customers(filter); + } + + + +} + diff --git a/src/data-consult/clientes.module.ts b/src/data-consult/clientes.module.ts new file mode 100644 index 0000000..8f02ed7 --- /dev/null +++ b/src/data-consult/clientes.module.ts @@ -0,0 +1,19 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { clientesService } from './clientes.service'; +import { clientesController } from './clientes.controller'; + +/* +https://docs.nestjs.com/modules +*/ + +import { Module } from '@nestjs/common'; + +@Module({ + imports: [], + controllers: [ + clientesController,], + providers: [ + clientesService,], +}) +export class clientes { } diff --git a/src/data-consult/clientes.service.ts b/src/data-consult/clientes.service.ts new file mode 100644 index 0000000..0bf400f --- /dev/null +++ b/src/data-consult/clientes.service.ts @@ -0,0 +1,154 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { QueryRunner, DataSource } from 'typeorm'; +import { DATA_SOURCE } from '../core/constants'; +import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider'; +import { IRedisClient } from '../core/configs/cache/IRedisClient'; +import { getOrSetCache } from '../shared/cache.util'; + +@Injectable() +export class clientesService { + private readonly CUSTOMERS_TTL = 60 * 60 * 12; // 12 horas + private readonly CUSTOMERS_CACHE_KEY = 'clientes:search'; + + constructor( + @Inject(DATA_SOURCE) + private readonly dataSource: DataSource, + @Inject(RedisClientToken) + private readonly redisClient: IRedisClient, + ) {} + + /** + * Buscar clientes com cache otimizado + * @param filter - Filtro de busca (código, CPF/CNPJ ou nome) + * @returns Array de clientes encontrados + */ + async customers(filter: string) { + const cacheKey = `${this.CUSTOMERS_CACHE_KEY}:${filter}`; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.CUSTOMERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + // Primeira tentativa: busca por código do cliente + let sql = `SELECT PCCLIENT.CODCLI as "id" + ,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE|| + ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" + ,PCCLIENT.ESTCOB as "estcob" + FROM PCCLIENT + WHERE PCCLIENT.CODCLI = REGEXP_REPLACE('${filter}', '[^0-9]', '') + ORDER BY PCCLIENT.CLIENTE`; + let customers = await queryRunner.manager.query(sql); + + // Segunda tentativa: busca por CPF/CNPJ se não encontrou por código + if (customers.length === 0) { + sql = `SELECT PCCLIENT.CODCLI as "id", + PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE|| + ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" + ,PCCLIENT.ESTCOB as "estcob" + FROM PCCLIENT + WHERE REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '') = REGEXP_REPLACE('${filter}', '[^0-9]', '') + ORDER BY PCCLIENT.CLIENTE`; + customers = await queryRunner.manager.query(sql); + } + + // Terceira tentativa: busca por nome do cliente se não encontrou por código ou CPF/CNPJ + if (customers.length === 0) { + sql = `SELECT PCCLIENT.CODCLI as "id", + PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE|| + ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" + ,PCCLIENT.ESTCOB as "estcob" + FROM PCCLIENT + WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%' + ORDER BY PCCLIENT.CLIENTE`; + customers = await queryRunner.manager.query(sql); + } + + return customers; + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Buscar todos os clientes com cache + * @returns Array de todos os clientes + */ + async getAllCustomers() { + const cacheKey = 'clientes:all'; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.CUSTOMERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + const sql = `SELECT PCCLIENT.CODCLI as "id" + ,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE|| + ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" + ,PCCLIENT.ESTCOB as "estcob" + FROM PCCLIENT + ORDER BY PCCLIENT.CLIENTE`; + + return await queryRunner.manager.query(sql); + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Buscar cliente por ID específico com cache + * @param customerId - ID do cliente + * @returns Cliente encontrado ou null + */ + async getCustomerById(customerId: string) { + const cacheKey = `clientes:id:${customerId}`; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.CUSTOMERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + const sql = `SELECT PCCLIENT.CODCLI as "id" + ,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE|| + ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" + ,PCCLIENT.ESTCOB as "estcob" + FROM PCCLIENT + WHERE PCCLIENT.CODCLI = '${customerId}'`; + + const customers = await queryRunner.manager.query(sql); + return customers.length > 0 ? customers[0] : null; + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Limpar cache de clientes (útil para invalidação) + * @param pattern - Padrão de chaves para limpar (opcional) + */ + async clearCustomersCache(pattern?: string) { + const cachePattern = pattern || 'clientes:*'; + + // Nota: Esta funcionalidade requer implementação específica do Redis + // Por enquanto, mantemos a interface para futuras implementações + console.log(`Cache de clientes seria limpo para o padrão: ${cachePattern}`); + } +} diff --git a/src/data-consult/data-consult.controller.ts b/src/data-consult/data-consult.controller.ts index cdf2109..8b255d1 100644 --- a/src/data-consult/data-consult.controller.ts +++ b/src/data-consult/data-consult.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe, ParseIntPipe } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { DataConsultService } from './data-consult.service'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard' import { ProductDto } from './dto/product.dto'; @@ -7,6 +7,7 @@ import { StoreDto } from './dto/store.dto'; import { SellerDto } from './dto/seller.dto'; import { BillingDto } from './dto/billing.dto'; import { CustomerDto } from './dto/customer.dto'; +import { CarrierDto, FindCarriersDto } from './dto/carrier.dto'; @ApiTags('DataConsult') @Controller('api/v1/data-consult') @@ -24,7 +25,7 @@ export class DataConsultController { } @UseGuards(JwtAuthGuard) - @ApiBearerAuth() + @ApiBearerAuth() @Get('sellers') @ApiOperation({ summary: 'Lista todos os vendedores' }) @ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] }) @@ -70,5 +71,36 @@ export class DataConsultController { async getAllProducts(): Promise { return this.dataConsultService.getAllProducts(); } - + + @Get('carriers/all') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' }) + @ApiResponse({ status: 200, description: 'Lista de transportadoras retornada com sucesso', type: [CarrierDto] }) + @UsePipes(new ValidationPipe({ transform: true })) + async getAllCarriers(): Promise { + return this.dataConsultService.getAllCarriers(); + } + + @Get('carriers') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Busca transportadoras por período de data' }) + @ApiResponse({ status: 200, description: 'Lista de transportadoras por período retornada com sucesso', type: [CarrierDto] }) + @UsePipes(new ValidationPipe({ transform: true })) + async getCarriersByDate(@Query() query: FindCarriersDto): Promise { + return this.dataConsultService.getCarriersByDate(query); + } + + @Get('carriers/order/:orderId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Busca transportadoras de um pedido específico' }) + @ApiParam({ name: 'orderId', example: 236001388 }) + @ApiResponse({ status: 200, description: 'Lista de transportadoras do pedido retornada com sucesso', type: [CarrierDto] }) + @UsePipes(new ValidationPipe({ transform: true })) + async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise { + return this.dataConsultService.getOrderCarriers(orderId); + } + } \ No newline at end of file diff --git a/src/data-consult/data-consult.module.ts b/src/data-consult/data-consult.module.ts index d00c750..15267e7 100644 --- a/src/data-consult/data-consult.module.ts +++ b/src/data-consult/data-consult.module.ts @@ -5,9 +5,10 @@ import { DataConsultRepository } from './data-consult.repository'; import { LoggerModule } from 'src/Log/logger.module'; import { ConfigModule } from '@nestjs/config'; import { RedisModule } from 'src/core/configs/cache/redis.module'; +import { clientes } from './clientes.module'; @Module({ - imports: [LoggerModule, ConfigModule, RedisModule], + imports: [LoggerModule, ConfigModule, RedisModule, clientes], controllers: [DataConsultController], providers: [ DataConsultService, diff --git a/src/data-consult/data-consult.repository.ts b/src/data-consult/data-consult.repository.ts index 3dade38..19862d7 100644 --- a/src/data-consult/data-consult.repository.ts +++ b/src/data-consult/data-consult.repository.ts @@ -40,7 +40,7 @@ export class DataConsultRepository { return results.map(result => new StoreDto(result)); } - async findSellers(): Promise { +async findSellers(): Promise { const sql = ` SELECT PCUSUARI.CODUSUR as "id", PCUSUARI.NOME as "name" @@ -55,51 +55,166 @@ export class DataConsultRepository { async findBillings(): Promise { const sql = ` - SELECT PCPEDC.NUMPED as "id", - PCPEDC.DATA as "date", - PCPEDC.VLTOTAL as "total" - FROM PCPEDC - WHERE PCPEDC.POSICAO = 'F' + SELECT p.CODCOB, p.COBRANCA FROM PCCOB p `; const results = await this.executeQuery(sql); return results.map(result => new BillingDto(result)); } async findCustomers(filter: string): Promise { - const sql = ` - SELECT PCCLIENT.CODCLI as "id", - PCCLIENT.CLIENTE as "name", - PCCLIENT.CGCENT as "document" - FROM PCCLIENT - WHERE PCCLIENT.CLIENTE LIKE :filter - OR PCCLIENT.CGCENT LIKE :filter + // 1) limpa todos os não-dígitos para buscas exatas + const cleanedDigits = filter.replace(/\D/g, ''); + + // 2) prepara filtro para busca por nome (LIKE) + const likeFilter = `%${filter.toUpperCase().replace(/@/g, '%')}%`; + + let customers: CustomerDto[] = []; + + // --- 1ª tentativa: busca por código do cliente (CODCLI) --- + let sql = ` + SELECT + PCCLIENT.CODCLI AS "id", + PCCLIENT.CLIENTE AS "name", + REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document", + PCCLIENT.ESTCOB AS "estcob" + FROM PCCLIENT + WHERE PCCLIENT.CODCLI = :0 + ORDER BY PCCLIENT.CLIENTE `; - const results = await this.executeQuery(sql, [`%${filter}%`]); - return results.map(result => new CustomerDto(result)); + customers = await this.executeQuery(sql, [cleanedDigits]); + + // --- 2ª tentativa: busca por CPF/CNPJ (CGCENT) --- + if (customers.length === 0) { + sql = ` + SELECT + PCCLIENT.CODCLI AS "id", + PCCLIENT.CLIENTE AS "name", + REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document", + PCCLIENT.ESTCOB AS "estcob" + FROM PCCLIENT + WHERE REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') = :0 + ORDER BY PCCLIENT.CLIENTE + `; + customers = await this.executeQuery(sql, [cleanedDigits]); + } + + // --- 3ª tentativa: busca parcial por nome --- + if (customers.length === 0) { + sql = ` + SELECT + PCCLIENT.CODCLI AS "id", + PCCLIENT.CLIENTE AS "name", + REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document", + PCCLIENT.ESTCOB AS "estcob" + FROM PCCLIENT + WHERE UPPER(PCCLIENT.CLIENTE) LIKE :0 + ORDER BY PCCLIENT.CLIENTE + `; + customers = await this.executeQuery(sql, [likeFilter]); + } + + return customers.map(row => new CustomerDto(row)); } async findProducts(filter: string): Promise { const sql = ` SELECT PCPRODUT.CODPROD as "id", - PCPRODUT.DESCRICAO as "name", - PCPRODUT.CODFAB as "manufacturerCode" + PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description" FROM PCPRODUT - WHERE PCPRODUT.DESCRICAO LIKE :filter - OR PCPRODUT.CODFAB LIKE :filter + WHERE PCPRODUT.CODPROD = :filter `; - const results = await this.executeQuery(sql, [`%${filter}%`]); + const results = await this.executeQuery(sql, [filter]); return results.map(result => new ProductDto(result)); } async findAllProducts(): Promise { const sql = ` SELECT PCPRODUT.CODPROD as "id", - PCPRODUT.DESCRICAO as "name", - PCPRODUT.CODFAB as "manufacturerCode" + PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description" FROM PCPRODUT WHERE ROWNUM <= 500 `; const results = await this.executeQuery(sql); return results.map(result => new ProductDto(result)); } -} \ No newline at end of file + + /** + * Busca todas as transportadoras cadastradas no sistema + */ + async findAllCarriers(): Promise { + const sql = ` + SELECT DISTINCT + PCFORNEC.CODFORNEC as "carrierId", + PCFORNEC.FORNECEDOR as "carrierName", + PCFORNEC.CODFORNEC || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription" + FROM PCFORNEC + WHERE PCFORNEC.CODFORNEC IS NOT NULL + AND PCFORNEC.CODFORNEC > 0 + AND PCFORNEC.FORNECEDOR IS NOT NULL + ORDER BY PCFORNEC.FORNECEDOR + `; + return await this.executeQuery(sql); + } + + /** + * Busca as transportadoras por período de data + */ + async findCarriersByDate(query: any): Promise { + let sql = ` + SELECT DISTINCT + PCPEDC.CODFORNECFRETE as "carrierId", + PCFORNEC.FORNECEDOR as "carrierName", + PCPEDC.CODFORNECFRETE || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription", + COUNT(PCPEDC.NUMPED) as "ordersCount" + FROM PCPEDC + LEFT JOIN PCFORNEC ON PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC + WHERE PCPEDC.CODFORNECFRETE IS NOT NULL + AND PCPEDC.CODFORNECFRETE > 0 + `; + + const conditions: string[] = []; + const parameters: any[] = []; + let paramIndex = 0; + + if (query.dateIni) { + conditions.push(`AND PCPEDC.DATA >= TO_DATE(:${paramIndex}, 'YYYY-MM-DD')`); + parameters.push(query.dateIni); + paramIndex++; + } + if (query.dateEnd) { + conditions.push(`AND PCPEDC.DATA <= TO_DATE(:${paramIndex}, 'YYYY-MM-DD')`); + parameters.push(query.dateEnd); + paramIndex++; + } + if (query.codfilial) { + conditions.push(`AND PCPEDC.CODFILIAL = :${paramIndex}`); + parameters.push(query.codfilial); + paramIndex++; + } + + sql += "\n" + conditions.join("\n"); + sql += "\nGROUP BY PCPEDC.CODFORNECFRETE, PCFORNEC.FORNECEDOR"; + sql += "\nORDER BY PCPEDC.CODFORNECFRETE"; + + return await this.executeQuery(sql, parameters); + } + + /** + * Busca as transportadoras de um pedido específico + */ + async findOrderCarriers(orderId: number): Promise { + const sql = ` + SELECT DISTINCT + PCPEDC.CODFORNECFRETE as "carrierId", + PCFORNEC.FORNECEDOR as "carrierName", + PCPEDC.CODFORNECFRETE || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription" + FROM PCPEDC + LEFT JOIN PCFORNEC ON PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC + WHERE PCPEDC.NUMPED = :0 + AND PCPEDC.CODFORNECFRETE IS NOT NULL + AND PCPEDC.CODFORNECFRETE > 0 + ORDER BY PCPEDC.CODFORNECFRETE + `; + return await this.executeQuery(sql, [orderId]); + } +} \ No newline at end of file diff --git a/src/data-consult/data-consult.service.ts b/src/data-consult/data-consult.service.ts index 81231ba..e695650 100644 --- a/src/data-consult/data-consult.service.ts +++ b/src/data-consult/data-consult.service.ts @@ -5,6 +5,7 @@ import { SellerDto } from './dto/seller.dto'; import { BillingDto } from './dto/billing.dto'; import { CustomerDto } from './dto/customer.dto'; import { ProductDto } from './dto/product.dto'; +import { CarrierDto, FindCarriersDto } from './dto/carrier.dto'; import { ILogger } from '../Log/ILogger'; import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider'; import { IRedisClient } from '../core/configs/cache/IRedisClient'; @@ -15,11 +16,14 @@ import { DATA_SOURCE } from '../core/constants'; @Injectable() export class DataConsultService { private readonly SELLERS_CACHE_KEY = 'data-consult:sellers'; - private readonly SELLERS_TTL = 3600; + private readonly SELLERS_TTL = 3600; private readonly STORES_TTL = 3600; private readonly BILLINGS_TTL = 3600; private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all'; - private readonly ALL_PRODUCTS_TTL = 600; + private readonly ALL_PRODUCTS_TTL = 600; + private readonly CUSTOMERS_TTL = 3600; + private readonly CARRIERS_CACHE_KEY = 'data-consult:carriers:all'; + private readonly CARRIERS_TTL = 3600; constructor( private readonly repository: DataConsultRepository, @@ -63,7 +67,7 @@ export class DataConsultService { this.logger.error('Erro ao buscar vendedores', error); throw new HttpException('Erro ao buscar vendedores', HttpStatus.INTERNAL_SERVER_ERROR); } - } + } /** * @returns Array de BillingDto @@ -134,4 +138,71 @@ export class DataConsultService { throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR); } } -} + + /** + * Obter todas as transportadoras cadastradas + * @returns Array de CarrierDto + */ + async getAllCarriers(): Promise { + this.logger.log('Buscando todas as transportadoras'); + try { + return getOrSetCache( + this.redisClient, + this.CARRIERS_CACHE_KEY, + this.CARRIERS_TTL, + async () => { + const carriers = await this.repository.findAllCarriers(); + return carriers.map(carrier => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + })); + } + ); + } catch (error) { + this.logger.error('Erro ao buscar transportadoras', error); + throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Obter transportadoras por período de data + * @param query - Filtros de data e filial + * @returns Array de CarrierDto + */ + async getCarriersByDate(query: FindCarriersDto): Promise { + this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`); + try { + const carriers = await this.repository.findCarriersByDate(query); + return carriers.map(carrier => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + ordersCount: carrier.ordersCount ? Number(carrier.ordersCount) : 0, + })); + } catch (error) { + this.logger.error('Erro ao buscar transportadoras por período', error); + throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Obter transportadoras de um pedido específico + * @param orderId - ID do pedido + * @returns Array de CarrierDto + */ + async getOrderCarriers(orderId: number): Promise { + this.logger.log(`Buscando transportadoras do pedido: ${orderId}`); + try { + const carriers = await this.repository.findOrderCarriers(orderId); + return carriers.map(carrier => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + })); + } catch (error) { + this.logger.error('Erro ao buscar transportadoras do pedido', error); + throw new HttpException('Erro ao buscar transportadoras do pedido', HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file diff --git a/src/data-consult/dto/carrier.dto.ts b/src/data-consult/dto/carrier.dto.ts new file mode 100644 index 0000000..3beb367 --- /dev/null +++ b/src/data-consult/dto/carrier.dto.ts @@ -0,0 +1,58 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, IsDateString } from 'class-validator'; + +export class CarrierDto { + @ApiProperty({ + description: 'ID da transportadora', + example: '123' + }) + carrierId: string; + + @ApiProperty({ + description: 'Nome da transportadora', + example: 'TRANSPORTADORA ABC LTDA' + }) + carrierName: string; + + @ApiProperty({ + description: 'Descrição completa da transportadora (ID - Nome)', + example: '123 - TRANSPORTADORA ABC LTDA' + }) + carrierDescription: string; + + @ApiProperty({ + description: 'Quantidade de pedidos da transportadora no período', + example: 15, + required: false + }) + ordersCount?: number; +} + +export class FindCarriersDto { + @ApiProperty({ + description: 'Data inicial para filtro (formato YYYY-MM-DD)', + example: '2024-01-01', + required: false + }) + @IsOptional() + @IsDateString() + dateIni?: string; + + @ApiProperty({ + description: 'Data final para filtro (formato YYYY-MM-DD)', + example: '2024-12-31', + required: false + }) + @IsOptional() + @IsDateString() + dateEnd?: string; + + @ApiProperty({ + description: 'ID da filial', + example: '1', + required: false + }) + @IsOptional() + @IsString() + codfilial?: string; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7d4d142..9a9ff5c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,11 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; async function bootstrap() { + /** + * Configura timezone para horário brasileiro + */ + process.env.TZ = 'America/Sao_Paulo'; + const app = await NestFactory.create(AppModule); app.use(helmet({ @@ -46,8 +51,8 @@ async function bootstrap() { app.enableCors({ origin: process.env.NODE_ENV === 'production' - ? ['https://seu-dominio.com', 'https://admin.seu-dominio.com'] - : '*', + ? ['https://www.jurunense.com', 'https://*.jurunense.com'] + : ['http://localhost:9602 add '], methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], credentials: true, allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], diff --git a/src/orders/application/orders.service.ts b/src/orders/application/orders.service.ts index 3f0e995..d161b02 100644 --- a/src/orders/application/orders.service.ts +++ b/src/orders/application/orders.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@nestjs/common'; +import { Injectable, Inject, HttpStatus } from '@nestjs/common'; import { FindOrdersDto } from '../dto/find-orders.dto'; import { InvoiceDto } from '../dto/find-invoice.dto'; import { CutItemDto } from '../dto/CutItemDto'; @@ -12,14 +12,24 @@ import { OrderDeliveryDto } from '../dto/OrderDeliveryDto'; import { OrderTransferDto } from '../dto/OrderTransferDto'; import { OrderStatusDto } from '../dto/OrderStatusDto'; import { InvoiceCheckDto } from '../dto/invoice-check.dto'; - +import { LeadtimeDto } from '../dto/leadtime.dto'; +import { HttpException } from '@nestjs/common/exceptions/http.exception'; +import { CarrierDto } from '../../data-consult/dto/carrier.dto'; +import { MarkData } from '../interface/markdata'; +import { EstLogTransferFilterDto, EstLogTransferResponseDto } from '../dto/estlogtransfer.dto'; @Injectable() export class OrdersService { - private readonly TTL_ORDERS = 60 * 10; // 10 minutos + private readonly TTL_ORDERS = 60 * 30; // 30 minutos private readonly TTL_INVOICE = 60 * 60; // 1 hora private readonly TTL_ITENS = 60 * 10; // 10 minutos + private readonly TTL_LEADTIME = 60 * 360; // 6 horas + private readonly TTL_DELIVERIES = 60 * 10; // 10 minutos + private readonly TTL_TRANSFER = 60 * 15; // 15 minutos + private readonly TTL_STATUS = 60 * 5; // 5 minutos + private readonly TTL_CARRIERS = 60 * 20; // 20 minutos + private readonly TTL_MARKS = 60 * 25; // 25 minutos constructor( private readonly ordersRepository: OrdersRepository, @@ -31,7 +41,6 @@ export class OrdersService { */ async findOrders(query: FindOrdersDto) { const key = `orders:query:${this.hashObject(query)}`; - return getOrSetCache( this.redisClient, key, @@ -40,6 +49,65 @@ export class OrdersService { ); } + /** + * Buscar pedidos por data de entrega com cache + */ + async findOrdersByDeliveryDate(query: any) { + const key = `orders:delivery:${this.hashObject(query)}`; + return getOrSetCache( + this.redisClient, + key, + this.TTL_ORDERS, + () => this.ordersRepository.findOrdersByDeliveryDate(query), + ); + } + + /** + * Buscar pedidos com resultados de fechamento de caixa + */ + async findOrdersWithCheckout(query: FindOrdersDto) { + const key = `orders:checkout:${this.hashObject(query)}`; + return getOrSetCache( + this.redisClient, + key, + this.TTL_ORDERS, + async () => { + // Primeiro obtém a lista de pedidos + const orders = await this.findOrders(query); + // Para cada pedido, busca o fechamento de caixa + const results = await Promise.all( + orders.map(async order => { + try { + const checkout = await this.ordersRepository.findOrderWithCheckoutByOrder( + Number(order.orderId), + ); + return { ...order, checkout }; + } catch { + return { ...order, checkout: null }; + } + }), + ); + return results; + } + ); + } + + async getOrderCheckout(orderId: number) { + const key = `orders:checkout:${orderId}`; + return getOrSetCache( + this.redisClient, + key, + this.TTL_ORDERS, + async () => { + const result = await this.ordersRepository.findOrderWithCheckoutByOrder(orderId); + if (!result) { + throw new HttpException('Nenhum fechamento encontrado', HttpStatus.NOT_FOUND); + } + return result; + } + ); + } + /** * Buscar nota fiscal por chave NFe com cache */ @@ -93,42 +161,169 @@ export class OrdersService { }); } - async getCutItens(orderId: string): Promise { - const itens = await this.ordersRepository.getCutItens(orderId); + /** + * Buscar entregas do pedido com cache + */ + async getOrderDeliveries( + orderId: string, + query: { createDateIni: string; createDateEnd: string }, + ): Promise { + const key = `orders:deliveries:${orderId}:${query.createDateIni}:${query.createDateEnd}`; - return itens.map(item => ({ - productId: Number(item.productId), - description: item.description, - pacth: item.pacth, - stockId: Number(item.stockId), - saleQuantity: Number(item.saleQuantity), - cutQuantity: Number(item.cutQuantity), - separedQuantity: Number(item.separedQuantity), - })); + return getOrSetCache( + this.redisClient, + key, + this.TTL_DELIVERIES, + () => this.ordersRepository.getOrderDeliveries(orderId, query), + ); + } + + async getCutItens(orderId: string): Promise { + const key = `orders:cutitens:${orderId}`; + + return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => { + const itens = await this.ordersRepository.getCutItens(orderId); + + return itens.map(item => ({ + productId: Number(item.productId), + description: item.description, + pacth: item.pacth, + stockId: Number(item.stockId), + saleQuantity: Number(item.saleQuantity), + cutQuantity: Number(item.cutQuantity), + separedQuantity: Number(item.separedQuantity), + })); + }); } async getOrderDelivery(orderId: string): Promise { - return this.ordersRepository.getOrderDelivery(orderId); + const key = `orders:delivery:${orderId}`; + + return getOrSetCache(this.redisClient, key, this.TTL_DELIVERIES, () => + this.ordersRepository.getOrderDelivery(orderId), + ); + } + + /** + * Buscar leadtime do pedido com cache + */ + async getLeadtime(orderId: string): Promise { + const key = `orders:leadtime:${orderId}`; + return getOrSetCache( + this.redisClient, + key, + this.TTL_LEADTIME, + () => this.ordersRepository.getLeadtimeWMS(orderId) + ); } async getTransfer(orderId: number): Promise { - return this.ordersRepository.getTransfer(orderId); + const key = `orders:transfer:${orderId}`; + + return getOrSetCache(this.redisClient, key, this.TTL_TRANSFER, () => + this.ordersRepository.getTransfer(orderId), + ); + } + + /** + * Buscar log de transferência por ID do pedido com cache + */ + async getTransferLog( + orderId: number, + filters?: EstLogTransferFilterDto + ): Promise { + const key = `orders:transfer-log:${orderId}:${this.hashObject(filters || {})}`; + + return getOrSetCache(this.redisClient, key, this.TTL_TRANSFER, () => + this.ordersRepository.estlogtransfer(orderId, filters), + ); + } + + /** + * Buscar logs de transferência com filtros (sem especificar pedido específico) + */ + async getTransferLogs( + filters?: EstLogTransferFilterDto + ): Promise { + const key = `orders:transfer-logs:${this.hashObject(filters || {})}`; + + return getOrSetCache(this.redisClient, key, this.TTL_TRANSFER, () => + this.ordersRepository.estlogtransfers(filters), + ); } async getStatusOrder(orderId: number): Promise { - return this.ordersRepository.getStatusOrder(orderId); + const key = `orders:status:${orderId}`; + + return getOrSetCache(this.redisClient, key, this.TTL_STATUS, () => + this.ordersRepository.getStatusOrder(orderId), + ); } + /** - * Utilitário para gerar hash MD5 de objetos + * Utilitário para gerar hash MD5 de objetos */ private hashObject(obj: any): string { const str = JSON.stringify(obj, Object.keys(obj).sort()); return createHash('md5').update(str).digest('hex'); } + async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> { + // Não usa cache para operações de escrita + return this.ordersRepository.createInvoiceCheck(invoice); + } -async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> { - return this.ordersRepository.createInvoiceCheck(invoice); -} + /** + * Buscar transportadoras do pedido com cache + */ + async getOrderCarriers(orderId: number): Promise { + const key = `orders:carriers:${orderId}`; + return getOrSetCache(this.redisClient, key, this.TTL_CARRIERS, async () => { + const carriers = await this.ordersRepository.getOrderCarriers(orderId); + + return carriers.map(carrier => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + })); + }); + } + + /** + * Buscar marca por ID com cache + */ + async findOrderByMark(orderId: number): Promise { + const key = `orders:mark:${orderId}`; + + return getOrSetCache(this.redisClient, key, this.TTL_MARKS, async () => { + const result = await this.ordersRepository.findorderbymark(orderId); + if (!result) { + throw new HttpException('Marca não encontrada', HttpStatus.NOT_FOUND); + } + return result; + }); + } + + /** + * Buscar todas as marcas disponíveis com cache + */ + async getAllMarks(): Promise { + const key = 'orders:marks:all'; + + return getOrSetCache(this.redisClient, key, this.TTL_MARKS, async () => { + return await this.ordersRepository.getAllMarks(); + }); + } + + /** + * Buscar marcas por nome com cache + */ + async getMarksByName(markName: string): Promise { + const key = `orders:marks:name:${markName}`; + + return getOrSetCache(this.redisClient, key, this.TTL_MARKS, async () => { + return await this.ordersRepository.getMarksByName(markName); + }); + } } \ No newline at end of file diff --git a/src/orders/controllers/orders.controller.ts b/src/orders/controllers/orders.controller.ts index 0540566..a5defac 100644 --- a/src/orders/controllers/orders.controller.ts +++ b/src/orders/controllers/orders.controller.ts @@ -11,39 +11,96 @@ import { ValidationPipe, HttpException, HttpStatus, + DefaultValuePipe, + ParseBoolPipe, } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags, ApiQuery, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ResponseInterceptor } from '../../common/response.interceptor'; import { OrdersService } from '../application/orders.service'; import { FindOrdersDto } from '../dto/find-orders.dto'; -import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto'; +import { JwtAuthGuard, } from 'src/auth/guards/jwt-auth.guard'; import { InvoiceDto } from '../dto/find-invoice.dto'; import { OrderItemDto } from "../dto/OrderItemDto"; +import { LeadtimeDto } from '../dto/leadtime.dto'; import { CutItemDto } from '../dto/CutItemDto'; import { OrderDeliveryDto } from '../dto/OrderDeliveryDto'; import { OrderTransferDto } from '../dto/OrderTransferDto'; import { OrderStatusDto } from '../dto/OrderStatusDto'; import { InvoiceCheckDto } from '../dto/invoice-check.dto'; - - - +import { ParseIntPipe } from '@nestjs/common/pipes/parse-int.pipe'; +import { CarrierDto } from 'src/data-consult/dto/carrier.dto'; +import { OrderResponseDto } from '../dto/order-response.dto'; +import { MarkResponseDto } from '../dto/mark-response.dto'; +import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto'; @ApiTags('Orders') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) -@UseInterceptors(ResponseInterceptor) +//@ApiBearerAuth() +//@UseGuards(JwtAuthGuard) @Controller('api/v1/orders') export class OrdersController { constructor(private readonly ordersService: OrdersService) {} @Get('find') + @ApiOperation({ + summary: 'Busca pedidos', + description: 'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.' + }) + @ApiQuery({ name: 'includeCheckout', required: false, type: 'boolean', description: 'Incluir dados de checkout' }) + @ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' }) + @ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' }) + @ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' }) + @ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' }) @UsePipes(new ValidationPipe({ transform: true })) - findOrders(@Query() query: FindOrdersDto) { + @ApiResponse({ status: 200, description: 'Lista de pedidos retornada com sucesso', type: [OrderResponseDto] }) + findOrders( + @Query() query: FindOrdersDto, + @Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe) + includeCheckout: boolean, + ) { + if (includeCheckout) { + return this.ordersService.findOrdersWithCheckout(query); + } return this.ordersService.findOrders(query); } + @Get('find-by-delivery-date') + @ApiOperation({ + summary: 'Busca pedidos por data de entrega', + description: 'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.' + }) + @ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' }) + @ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' }) + @ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' }) + @ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' }) + @UsePipes(new ValidationPipe({ transform: true })) + @ApiResponse({ status: 200, description: 'Lista de pedidos por data de entrega retornada com sucesso', type: [OrderResponseDto] }) + findOrdersByDeliveryDate( + @Query() query: FindOrdersByDeliveryDateDto, + ) { + return this.ordersService.findOrdersByDeliveryDate(query); + } + + @Get(':orderId/checkout') + @ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' }) + @ApiParam({ name: 'orderId', example: 236001388 }) + @UsePipes(new ValidationPipe({ transform: true })) + getOrderCheckout( + @Param('orderId', ParseIntPipe) orderId: number, + ) { + return this.ordersService.getOrderCheckout(orderId); + } + + @Get('invoice/:chavenfe') + @ApiParam({ + name: 'chavenfe', + required: true, + description: 'Chave da Nota Fiscal (44 dígitos)', + example: '35191234567890000123550010000000011000000010', + }) + @ApiOperation({ summary: 'Busca NF pela chave' }) @UsePipes(new ValidationPipe({ transform: true })) async getInvoice(@Param('chavenfe') chavenfe: string): Promise { @@ -56,12 +113,14 @@ export class OrdersController { ); } } + @Get('itens/:orderId') @ApiOperation({ summary: 'Busca PELO numero do pedido' }) + @ApiParam({ name: 'orderId', example: '236001388' }) @UsePipes(new ValidationPipe({ transform: true })) - async getItens(@Param('orderId') orderId: string): Promise { + async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise { try { - return await this.ordersService.getItens(orderId); + return await this.ordersService.getItens(orderId.toString()); } catch (error) { throw new HttpException( error.message || 'Erro ao buscar itens do pedido', @@ -71,10 +130,11 @@ export class OrdersController { } @Get('cut-itens/:orderId') @ApiOperation({ summary: 'Busca itens cortados do pedido' }) + @ApiParam({ name: 'orderId', example: '236001388' }) @UsePipes(new ValidationPipe({ transform: true })) - async getCutItens(@Param('orderId') orderId: string): Promise { + async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise { try { - return await this.ordersService.getCutItens(orderId); + return await this.ordersService.getCutItens(orderId.toString()); } catch (error) { throw new HttpException( error.message || 'Erro ao buscar itens cortados', @@ -82,13 +142,14 @@ export class OrdersController { ); } } - + @Get('delivery/:orderId') @ApiOperation({ summary: 'Busca dados de entrega do pedido' }) + @ApiParam({ name: 'orderId', example: '236001388' }) @UsePipes(new ValidationPipe({ transform: true })) - async getOrderDelivery(@Param('orderId') orderId: string): Promise { + async getOrderDelivery(@Param('orderId', ParseIntPipe) orderId: number): Promise { try { - return await this.ordersService.getOrderDelivery(orderId); + return await this.ordersService.getOrderDelivery(orderId.toString()); } catch (error) { throw new HttpException( error.message || 'Erro ao buscar dados de entrega', @@ -98,9 +159,10 @@ export class OrdersController { } @Get('transfer/:orderId') -@ApiOperation({ summary: 'Consulta pedidos de transferência' }) + @ApiOperation({ summary: 'Consulta pedidos de transferência' }) + @ApiParam({ name: 'orderId', example: 236001388 }) @UsePipes(new ValidationPipe({ transform: true })) -async getTransfer(@Param('orderId') orderId: number): Promise { + async getTransfer(@Param('orderId', ParseIntPipe) orderId: number): Promise { try { return await this.ordersService.getTransfer(orderId); } catch (error) { @@ -113,8 +175,9 @@ async getTransfer(@Param('orderId') orderId: number): Promise { + async getStatusOrder(@Param('orderId', ParseIntPipe) orderId: number): Promise { try { return await this.ordersService.getStatusOrder(orderId); } catch (error) { @@ -125,6 +188,43 @@ async getStatusOrder(@Param('orderId') orderId: number): Promise { + // Definir datas padrão caso não sejam fornecidas + const defaultDateIni = createDateIni || new Date(new Date().setDate(new Date().getDate() - 30)).toISOString().split('T')[0]; + const defaultDateEnd = createDateEnd || new Date().toISOString().split('T')[0]; + + return this.ordersService.getOrderDeliveries(orderId.toString(), { + createDateIni: defaultDateIni, + createDateEnd: defaultDateEnd, + }); + } + + +@Get('leadtime/:orderId') +@ApiOperation({ summary: 'Consulta leadtime do pedido' }) + @ApiParam({ name: 'orderId', example: '236001388' }) +@UsePipes(new ValidationPipe({ transform: true })) + async getLeadtime(@Param('orderId', ParseIntPipe) orderId: number): Promise { + try { + return await this.ordersService.getLeadtime(orderId.toString()); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar leadtime do pedido', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + @Post('invoice/check') @ApiOperation({ summary: 'Cria conferência de nota fiscal' }) @UsePipes(new ValidationPipe({ transform: true })) @@ -137,5 +237,147 @@ async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: s error.status || HttpStatus.INTERNAL_SERVER_ERROR ); } - } +} + +@Get('carriers/:orderId') +@ApiOperation({ summary: 'Busca transportadoras do pedido' }) +@ApiParam({ name: 'orderId', example: 236001388 }) +@UsePipes(new ValidationPipe({ transform: true })) +async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise { + try { + return await this.ordersService.getOrderCarriers(orderId); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar transportadoras do pedido', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + +@Get('mark/:orderId') +@ApiOperation({ summary: 'Busca marca por ID do pedido' }) +@ApiParam({ name: 'orderId', example: 236001388 }) +@UsePipes(new ValidationPipe({ transform: true })) +@ApiResponse({ status: 200, description: 'Marca encontrada com sucesso', type: MarkResponseDto }) +@ApiResponse({ status: 404, description: 'Marca não encontrada' }) +async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise { + try { + return await this.ordersService.findOrderByMark(orderId); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar marca do pedido', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + +@Get('marks') +@ApiOperation({ summary: 'Busca todas as marcas disponíveis' }) +@UsePipes(new ValidationPipe({ transform: true })) +@ApiResponse({ status: 200, description: 'Lista de marcas retornada com sucesso', type: [MarkResponseDto] }) +async getAllMarks(): Promise { + try { + return await this.ordersService.getAllMarks(); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar marcas', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + +@Get('marks/search') +@ApiOperation({ summary: 'Busca marcas por nome' }) +@ApiQuery({ name: 'name', required: true, type: 'string', description: 'Nome da marca para buscar' }) +@UsePipes(new ValidationPipe({ transform: true })) +@ApiResponse({ status: 200, description: 'Lista de marcas encontradas', type: [MarkResponseDto] }) +async getMarksByName(@Query('name') markName: string): Promise { + try { + return await this.ordersService.getMarksByName(markName); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar marcas', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + +@Get('transfer-log/:orderId') +@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' }) +@ApiParam({ name: 'orderId', example: 153068638, description: 'ID do pedido para buscar log de transferência' }) +@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' }) +@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' }) +@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' }) +@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' }) +@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' }) +@UsePipes(new ValidationPipe({ transform: true })) +@ApiResponse({ status: 200, description: 'Log de transferência encontrado com sucesso', type: [EstLogTransferResponseDto] }) +@ApiResponse({ status: 400, description: 'OrderId inválido' }) +@ApiResponse({ status: 404, description: 'Log de transferência não encontrado' }) +async getTransferLog( + @Param('orderId', ParseIntPipe) orderId: number, + @Query('dttransf') dttransf?: string, + @Query('codfilial') codfilial?: number, + @Query('codfilialdest') codfilialdest?: number, + @Query('numpedloja') numpedloja?: number, + @Query('numpedtransf') numpedtransf?: number, +) { + try { + const filters = { + dttransf, + codfilial, + codfilialdest, + numpedloja, + numpedtransf, + }; + + return await this.ordersService.getTransferLog(orderId, filters); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar log de transferência', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} + +@Get('transfer-log') +@ApiOperation({ summary: 'Busca logs de transferência com filtros' }) +@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' }) +@ApiQuery({ name: 'dttransfIni', required: false, type: 'string', description: 'Data de transferência inicial (formato YYYY-MM-DD)' }) +@ApiQuery({ name: 'dttransfEnd', required: false, type: 'string', description: 'Data de transferência final (formato YYYY-MM-DD)' }) +@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' }) +@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' }) +@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' }) +@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' }) +@UsePipes(new ValidationPipe({ transform: true })) +@ApiResponse({ status: 200, description: 'Logs de transferência encontrados com sucesso', type: [EstLogTransferResponseDto] }) +@ApiResponse({ status: 400, description: 'Filtros inválidos' }) +async getTransferLogs( + @Query('dttransf') dttransf?: string, + @Query('dttransfIni') dttransfIni?: string, + @Query('dttransfEnd') dttransfEnd?: string, + @Query('codfilial') codfilial?: number, + @Query('codfilialdest') codfilialdest?: number, + @Query('numpedloja') numpedloja?: number, + @Query('numpedtransf') numpedtransf?: number, +) { + try { + const filters = { + dttransf, + dttransfIni, + dttransfEnd, + codfilial, + codfilialdest, + numpedloja, + numpedtransf, + }; + + return await this.ordersService.getTransferLogs(filters); + } catch (error) { + throw new HttpException( + error.message || 'Erro ao buscar logs de transferência', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } +} } diff --git a/src/orders/dto/emitente.dto.ts b/src/orders/dto/emitente.dto.ts new file mode 100644 index 0000000..4a6ca15 --- /dev/null +++ b/src/orders/dto/emitente.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * DTO para dados do emitente da nota fiscal + */ +export class EmitenteDto { + @ApiProperty({ + description: 'Código do emitente da nota fiscal', + example: 32, + nullable: true, + }) + codEmitente: number | null; + + @ApiProperty({ + description: 'Matrícula do funcionário emitente', + example: 32, + nullable: true, + }) + emitenteMatricula: number | null; + + @ApiProperty({ + description: 'Nome do funcionário emitente', + example: 'João Silva', + nullable: true, + }) + emitenteNome: string | null; +} diff --git a/src/orders/dto/estlogtransfer.dto.ts b/src/orders/dto/estlogtransfer.dto.ts new file mode 100644 index 0000000..9269052 --- /dev/null +++ b/src/orders/dto/estlogtransfer.dto.ts @@ -0,0 +1,110 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * DTO para filtros do log de transferência + */ +export class EstLogTransferFilterDto { + @ApiProperty({ + description: 'Data de transferência (formato YYYY-MM-DD)', + example: '2024-02-08', + required: false, + }) + dttransf?: string; + + @ApiProperty({ + description: 'Data de transferência inicial (formato YYYY-MM-DD)', + example: '2024-02-01', + required: false, + }) + dttransfIni?: string; + + @ApiProperty({ + description: 'Data de transferência final (formato YYYY-MM-DD)', + example: '2024-02-15', + required: false, + }) + dttransfEnd?: string; + + @ApiProperty({ + description: 'Código da filial de origem', + example: 6, + required: false, + }) + codfilial?: number; + + @ApiProperty({ + description: 'Código da filial de destino', + example: 4, + required: false, + }) + codfilialdest?: number; + + @ApiProperty({ + description: 'Número do pedido da loja', + example: 153068638, + required: false, + }) + numpedloja?: number; + + @ApiProperty({ + description: 'Número do pedido de transferência', + example: 153068637, + required: false, + }) + numpedtransf?: number; +} + +/** + * DTO para resposta do log de transferência + */ +export class EstLogTransferResponseDto { + @ApiProperty({ + description: 'Data de transferência', + example: '2024-02-08T03:00:00.000Z', + }) + DTTRANSF: string; + + @ApiProperty({ + description: 'Código da filial de origem', + example: 6, + }) + CODFILIAL: number; + + @ApiProperty({ + description: 'Código da filial de destino', + example: 4, + }) + CODFILIALDEST: number; + + @ApiProperty({ + description: 'Número do pedido da loja', + example: 153068638, + }) + NUMPEDLOJA: number; + + @ApiProperty({ + description: 'Número do pedido de transferência', + example: 153068637, + }) + NUMPEDTRANSF: number; + + @ApiProperty({ + description: 'Código do funcionário que fez a transferência', + example: 158, + }) + CODFUNCTRANSF: number; + + @ApiProperty({ + description: 'Número do pedido de recebimento de transferência', + example: null, + nullable: true, + }) + NUMPEDRCATRANSF: number | null; + + @ApiProperty({ + description: 'Status da transferência', + example: 'Em Trânsito', + nullable: true, + }) + statusTransfer: string | null; +} diff --git a/src/orders/dto/find-orders-by-delivery-date.dto.ts b/src/orders/dto/find-orders-by-delivery-date.dto.ts new file mode 100644 index 0000000..e4725e3 --- /dev/null +++ b/src/orders/dto/find-orders-by-delivery-date.dto.ts @@ -0,0 +1,113 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsOptional, + IsDateString, + IsString, + IsNumber, + IsIn, + IsBoolean +} from 'class-validator'; + +/** + * DTO para buscar pedidos por data de entrega + */ +export class FindOrdersByDeliveryDateDto { + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ + description: 'Data de entrega inicial (formato: YYYY-MM-DD)', + example: '2024-01-01' + }) + deliveryDateIni?: string; + + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ + description: 'Data de entrega final (formato: YYYY-MM-DD)', + example: '2024-12-31' + }) + deliveryDateEnd?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Código da filial', + example: '01' + }) + codfilial?: string; + + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: 'ID do vendedor', + example: 123 + }) + sellerId?: number; + + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: 'ID do cliente', + example: 456 + }) + customerId?: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Tipo de entrega (EN, EF, RP, RI)', + example: 'EN' + }) + deliveryType?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Status do pedido (L, P, B, M, F)', + example: 'L' + }) + status?: string; + + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: 'ID do pedido específico', + example: 236001388 + }) + orderId?: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Filtrar por status de transferência', + example: 'Em Trânsito,Em Separação,Aguardando Separação,Concluída', + enum: ['Em Trânsito', 'Em Separação', 'Aguardando Separação', 'Concluída'] + }) + statusTransfer?: string; + + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: 'ID da marca para filtrar pedidos', + example: 1 + }) + markId?: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Nome da marca para filtrar pedidos', + example: 'Nike' + }) + markName?: string; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + @ApiPropertyOptional({ + description: 'Filtrar pedidos que tenham registros na tabela de transfer log', + example: true + }) + hasPreBox?: boolean; +} \ No newline at end of file diff --git a/src/orders/dto/find-orders.dto.ts b/src/orders/dto/find-orders.dto.ts index 8e32ad7..03466d3 100644 --- a/src/orders/dto/find-orders.dto.ts +++ b/src/orders/dto/find-orders.dto.ts @@ -1,4 +1,8 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { CustomerDto } from 'src/data-consult/dto/customer.dto'; + import { IsOptional, @@ -6,6 +10,7 @@ import { IsNumber, IsDateString, IsIn, + IsBoolean } from 'class-validator'; export class FindOrdersDto { @@ -14,22 +19,68 @@ export class FindOrdersDto { @ApiPropertyOptional() codfilial?: string; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + includeCheckout?: boolean; + @IsOptional() @IsString() @ApiPropertyOptional() filialretira?: string; + @IsOptional() + @IsString() + @ApiPropertyOptional({ description: 'ID da transportadora para filtrar pedidos' }) + carrier?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + cnpj?: string; + @IsOptional() @IsNumber() @ApiPropertyOptional() + hour?: number; + @IsOptional() + @IsNumber() + @ApiPropertyOptional() + minute?: number; - sellerId?: number; + @IsOptional() + @IsString() + @ApiPropertyOptional() + partnerId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + customerName?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + stockId?: string; @IsOptional() @IsNumber() @ApiPropertyOptional() + sellerId?: number; + +@IsOptional() +@IsString() +@ApiPropertyOptional() +sellerName?: string; + + + + @IsOptional() + @IsNumber() + @ApiPropertyOptional() customerId?: number; @IsOptional() @@ -72,6 +123,11 @@ export class FindOrdersDto { @ApiPropertyOptional() invoiceDateEnd?: string; + @IsOptional() + @IsDateString() + @ApiPropertyOptional() + deliveryDate?: string; + @IsOptional() @IsNumber() @ApiPropertyOptional() @@ -80,20 +136,86 @@ export class FindOrdersDto { @IsOptional() @IsString() @ApiPropertyOptional() - deliveryType?: string; + deliveryType?: string; @IsOptional() @IsString() @ApiPropertyOptional() - status?: string; + status?: string; @IsOptional() @IsString() @ApiPropertyOptional() - type?: string; + type?: string; @IsOptional() - @IsIn(['S', 'N']) + @Type(() => Boolean) + @IsBoolean() @ApiPropertyOptional() - onlyPendentingTransfer?: string; + onlyPendingTransfer?: boolean; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Filtrar por status de transferência', + example: 'Em Trânsito,Em Separação,Aguardando Separação,Concluída', + enum: ['Em Trânsito', 'Em Separação', 'Aguardando Separação', 'Concluída'] + }) + statusTransfer?: string; + + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: 'ID da marca para filtrar pedidos', + }) + markId?: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Nome da marca para filtrar pedidos', + + }) + markName?: string; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + @ApiPropertyOptional({ + description: 'Filtrar pedidos que tenham registros na tabela de transfer log', + example: true + }) + hasPreBox?: boolean; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Código da filial de origem da transferência (Pre-Box)', + example: '5' + }) + preBoxFilial?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Código da filial de destino da transferência', + example: '6' + }) + transferDestFilial?: string; + + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ + description: 'Data de transferência inicial (formato YYYY-MM-DD)', + example: '2024-01-01' + }) + transferDateIni?: string; + + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ + description: 'Data de transferência final (formato YYYY-MM-DD)', + example: '2024-12-31' + }) + transferDateEnd?: string; } diff --git a/src/orders/dto/leadtime.dto.ts b/src/orders/dto/leadtime.dto.ts new file mode 100644 index 0000000..949c5e5 --- /dev/null +++ b/src/orders/dto/leadtime.dto.ts @@ -0,0 +1,9 @@ +export class LeadtimeDto { + orderId: number; + etapa: number; + descricaoEtapa: string; + data: Date | string; + codigoFuncionario: number | null; + nomeFuncionario: string | null; + numeroPedido: number; + } \ No newline at end of file diff --git a/src/orders/dto/mark-response.dto.ts b/src/orders/dto/mark-response.dto.ts new file mode 100644 index 0000000..2e41daf --- /dev/null +++ b/src/orders/dto/mark-response.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class MarkResponseDto { + @ApiProperty({ + description: 'Nome da marca', + example: 'Nike', + }) + MARCA: string; + + @ApiProperty({ + description: 'Código da marca', + example: 1, + }) + CODMARCA: number; + + @ApiProperty({ + description: 'Status ativo da marca (S/N)', + example: 'S', + }) + ATIVO: string; +} \ No newline at end of file diff --git a/src/orders/dto/order-delivery.dto.ts b/src/orders/dto/order-delivery.dto.ts new file mode 100644 index 0000000..edabe73 --- /dev/null +++ b/src/orders/dto/order-delivery.dto.ts @@ -0,0 +1,23 @@ + +export class OrderDeliveryDto { + storeId: number; + createDate: Date; + orderId: number; + orderIdSale: number | null; + deliveryDate: Date | null; + cnpj: string | null; + customerId: number; + customer: string; + deliveryType: string | null; + quantityItens: number; + status: string; + weight: number; + shipmentId: number; + driverId: number | null; + driverName: string | null; + carPlate: string | null; + carIdentification: string | null; + observation: string | null; + deliveryConfirmationDate: Date | null; + } + \ No newline at end of file diff --git a/src/orders/dto/order-response.dto.ts b/src/orders/dto/order-response.dto.ts new file mode 100644 index 0000000..f7a1b14 --- /dev/null +++ b/src/orders/dto/order-response.dto.ts @@ -0,0 +1,310 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class OrderResponseDto { + @ApiProperty({ + description: 'Data de criação do pedido', + example: '2024-04-02T10:00:00Z', + }) + createDate: Date; + + @ApiProperty({ + description: 'ID da loja', + example: '001 - Pre-Box (002)', + }) + storeId: string; + + @ApiProperty({ + description: 'ID do pedido', + example: 12345, + }) + orderId: number; + + @ApiProperty({ + description: 'ID do cliente', + example: '12345', + }) + customerId: string; + + @ApiProperty({ + description: 'Nome do cliente', + example: '12345 - João da Silva', + }) + customerName: string; + + @ApiProperty({ + description: 'ID do vendedor', + example: '001', + }) + sellerId: string; + + @ApiProperty({ + description: 'Nome do vendedor', + example: '001 - Maria Santos', + }) + sellerName: string; + + @ApiProperty({ + description: 'Nome da loja', + example: 'Loja Centro', + }) + store: string; + + @ApiProperty({ + description: 'Tipo de entrega', + example: 'Entrega (EN)', + }) + deliveryType: string; + + @ApiProperty({ + description: 'Local de entrega', + example: '001-Centro', + }) + deliveryLocal: string; + + @ApiProperty({ + description: 'Local de entrega principal', + example: '001-Rota Centro', + }) + masterDeliveryLocal: string; + + @ApiProperty({ + description: 'Tipo do pedido', + example: 'TV8 - Entrega (EN)', + }) + orderType: string; + + @ApiProperty({ + description: 'Valor total do pedido', + example: 1000.00, + }) + amount: number; + + @ApiProperty({ + description: 'Data de entrega', + example: '2024-04-05T10:00:00Z', + }) + deliveryDate: Date; + + @ApiProperty({ + description: 'Prioridade de entrega', + example: 'Alta', + }) + deliveryPriority: string; + + @ApiProperty({ + description: 'ID do carregamento', + example: 123, + }) + shipmentId: number; + + @ApiProperty({ + description: 'Data de liberação', + example: '2024-04-02T10:00:00Z', + }) + releaseDate: Date; + + @ApiProperty({ + description: 'Usuário de liberação', + example: '001', + }) + releaseUser: string; + + @ApiProperty({ + description: 'Nome do usuário de liberação', + example: '001 - João Silva', + }) + releaseUserName: string; + + @ApiProperty({ + description: 'Data de saída do carregamento', + example: '2024-04-03T10:00:00Z', + }) + shipmentDate: Date; + + @ApiProperty({ + description: 'Data de criação do carregamento', + example: '2024-04-02T10:00:00Z', + }) + shipmentDateCreate: Date; + + @ApiProperty({ + description: 'Data de fechamento do carregamento', + example: '2024-04-03T18:00:00Z', + }) + shipmentCloseDate: Date; + + @ApiProperty({ + description: 'ID do plano de pagamento', + example: '001', + }) + paymentId: string; + + @ApiProperty({ + description: 'Nome do plano de pagamento', + example: 'Cartão de Crédito', + }) + paymentName: string; + + @ApiProperty({ + description: 'ID da cobrança', + example: '001', + }) + billingId: string; + + @ApiProperty({ + description: 'Nome da cobrança', + example: '001 - Cartão de Crédito', + }) + billingName: string; + + @ApiProperty({ + description: 'Data de faturamento', + example: '2024-04-02T10:00:00Z', + }) + invoiceDate: Date; + + @ApiProperty({ + description: 'Hora de faturamento', + example: 10, + }) + invoiceHour: number; + + @ApiProperty({ + description: 'Minuto de faturamento', + example: 30, + }) + invoiceMinute: number; + + @ApiProperty({ + description: 'Número da nota fiscal', + example: 123456, + }) + invoiceNumber: number; + + @ApiProperty({ + description: 'Descrição do bloqueio', + example: 'Cliente com restrição', + }) + BloqDescription: string; + + @ApiProperty({ + description: 'Data de confirmação de entrega', + example: '2024-04-05T15:00:00Z', + }) + confirmDeliveryDate: Date; + + @ApiProperty({ + description: 'Peso total', + example: 50.5, + }) + totalWeigth: number; + + @ApiProperty({ + description: 'Processo do pedido', + example: 3, + }) + processOrder: number; + + @ApiProperty({ + description: 'Status do pedido', + example: 'FATURADO', + }) + status: string; + + @ApiProperty({ + description: 'Pagamento', + example: 0, + }) + payment: number; + + @ApiProperty({ + description: 'Motorista', + example: '001 - João Silva', + }) + driver: string; + + @ApiProperty({ + description: 'ID do pedido de venda', + example: 12346, + }) + orderSaleId: number; + + @ApiProperty({ + description: 'Descrição do carro', + example: 'Fiat Fiorino (ABC1234)', + }) + carDescription: string; + + @ApiProperty({ + description: 'Identificação do carro', + example: 'ABC1234', + }) + carIdentification: string; + + @ApiProperty({ + description: 'Transportadora', + example: '001 - Transportadora XYZ', + }) + carrier: string; + + @ApiProperty({ + description: 'Status da transferência', + example: 'Em Trânsito', + nullable: true, + }) + statusTransfer: string | null; + + @ApiProperty({ + description: 'Loja Pre-Box', + example: '002', + }) + storePreBox: string; + + @ApiProperty({ + description: 'Código do emitente da nota fiscal', + example: 32, + nullable: true, + }) + codEmitente: number | null; + + @ApiProperty({ + description: 'Matrícula do funcionário emitente', + example: 32, + nullable: true, + }) + emitenteMatricula: number | null; + + @ApiProperty({ + description: 'Nome do funcionário emitente', + example: 'João Silva', + nullable: true, + }) + emitenteNome: string | null; + + @ApiProperty({ + description: 'Código do funcionário de faturamento', + example: 1336, + nullable: true, + }) + fatUserCode: number | null; + + @ApiProperty({ + description: 'Nome do funcionário de faturamento', + example: 'ADRIANO COSTA DA SILVA', + nullable: true, + }) + fatUserName: string | null; + + @ApiProperty({ + description: 'Descrição completa do funcionário de faturamento', + example: '1336-ADRIANO COSTA DA SILVA', + nullable: true, + }) + fatUserDescription: string | null; + + @ApiProperty({ + description: 'Entrega agendada', + example: 'ENTREGA NORMAL', + }) + schedulerDelivery: string; +} \ No newline at end of file diff --git a/src/orders/interceptors/orders-response.interceptor.ts b/src/orders/interceptors/orders-response.interceptor.ts new file mode 100644 index 0000000..072da12 --- /dev/null +++ b/src/orders/interceptors/orders-response.interceptor.ts @@ -0,0 +1,20 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ResultModel } from '../../shared/ResultModel'; + +@Injectable() +export class OrdersResponseInterceptor implements NestInterceptor> { + intercept(context: ExecutionContext, next: CallHandler): Observable> { + return next.handle().pipe( + map((data) => { + return ResultModel.success(data); + }), + ); + } +} \ No newline at end of file diff --git a/src/orders/interface/markdata.ts b/src/orders/interface/markdata.ts new file mode 100644 index 0000000..60f8bda --- /dev/null +++ b/src/orders/interface/markdata.ts @@ -0,0 +1,5 @@ +export interface MarkData { + MARCA: string; + CODMARCA: number; + ATIVO: string; + } \ No newline at end of file diff --git a/src/orders/repositories/orders.repository.ts b/src/orders/repositories/orders.repository.ts index b3eef6c..4b722d1 100644 --- a/src/orders/repositories/orders.repository.ts +++ b/src/orders/repositories/orders.repository.ts @@ -1,4 +1,5 @@ import { Injectable, HttpException, HttpStatus } from "@nestjs/common"; +import { EstLogTransferFilterDto, EstLogTransferResponseDto } from "../dto/estlogtransfer.dto"; import { DataSource } from "typeorm"; import { InjectDataSource } from "@nestjs/typeorm"; import { FindOrdersDto } from "../dto/find-orders.dto"; @@ -8,143 +9,386 @@ import { OrderDeliveryDto } from '../dto/OrderDeliveryDto'; import { OrderTransferDto } from '../dto/OrderTransferDto'; import { OrderStatusDto } from '../dto/OrderStatusDto'; import { InvoiceCheckDto } from '../dto/invoice-check.dto'; - - +import { LeadtimeDto } from "../dto/leadtime.dto"; +import { MarkData } from "../interface/markdata"; @Injectable() export class OrdersRepository { constructor( - @InjectDataSource("oracle") private readonly dataSource: DataSource + @InjectDataSource("oracle") private readonly oracleDataSource: DataSource, + @InjectDataSource("postgres") private readonly postgresDataSource: DataSource ) {} - async findOrders(query: FindOrdersDto): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); + + + /** + * Busca log de transferência por ID do pedido + * @param orderId - ID do pedido + * @param filters - Filtros opcionais para a consulta + * @returns Dados do log de transferência ou null se não encontrado + */ + async estlogtransfer( + orderId: number, + filters?: EstLogTransferFilterDto + ): Promise { + if (!orderId || orderId <= 0) { + throw new HttpException('OrderId inválido', HttpStatus.BAD_REQUEST); + } + + let sql = ` +SELECT + E.DTTRANSF, + E.CODFILIAL, + E.CODFILIALDEST, + E.NUMPEDLOJA, + E.NUMPEDTRANSF, + E.CODFUNCTRANSF, + E.NUMPEDRCATRANSF, + CASE WHEN P.POSICAO = 'F' + AND P.CONDVENDA = 10 + AND (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = P.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = P.NUMNOTA) = 0 + AND P.CONDVENDA = 10 + AND P.POSICAO = 'F' THEN 'Em Trânsito' + WHEN P.POSICAO = 'M' + AND P.CONDVENDA = 10 THEN 'Em Separação' + WHEN P.POSICAO IN ( 'L', 'P' ) + AND P.CONDVENDA = 10 THEN 'Aguardando Separação' + WHEN P.CONDVENDA NOT IN ( 10 ) THEN NULL + ELSE 'Concluída' END as "statusTransfer" +FROM SEVEN.ESTLOGTRANSFCD E +LEFT JOIN PCPEDC P ON (E.NUMPEDTRANSF IS NOT NULL AND E.NUMPEDTRANSF = P.NUMPED) + OR (E.NUMPEDTRANSF IS NULL AND E.NUMPEDLOJA = P.NUMPED) +WHERE E.NUMPEDLOJA = :orderId + `; + + const parameters: any[] = [orderId]; + let paramIndex = 1; + + if (filters?.dttransf) { + sql += ` AND DTTRANSF = TO_DATE(:dttransf, 'YYYY-MM-DD')`; + parameters.push(filters.dttransf); + paramIndex++; + } + + if (filters?.codfilial) { + sql += ` AND CODFILIAL = :codfilial`; + parameters.push(filters.codfilial); + paramIndex++; + } + + if (filters?.codfilialdest) { + sql += ` AND CODFILIALDEST = :codfilialdest`; + parameters.push(filters.codfilialdest); + paramIndex++; + } + + if (filters?.numpedloja) { + sql += ` AND NUMPEDLOJA = :numpedloja`; + parameters.push(filters.numpedloja); + paramIndex++; + } + + if (filters?.numpedtransf) { + sql += ` AND NUMPEDTRANSF = :numpedtransf`; + parameters.push(filters.numpedtransf); + paramIndex++; + } + + sql += ` ORDER BY DTTRANSF DESC`; + + const result = await this.oracleDataSource.query(sql, parameters); + return result.length > 0 ? result : null; + } + + /** + * Busca logs de transferência com filtros (sem especificar pedido específico) + * @param filters - Filtros opcionais para a consulta + * @returns Dados dos logs de transferência ou null se não encontrado + */ + async estlogtransfers( + filters?: EstLogTransferFilterDto + ): Promise { + let sql = ` +SELECT + E.DTTRANSF, + E.CODFILIAL, + E.CODFILIALDEST, + E.NUMPEDLOJA, + E.NUMPEDTRANSF, + E.CODFUNCTRANSF, + E.NUMPEDRCATRANSF, + CASE WHEN P.POSICAO = 'F' + AND P.CONDVENDA = 10 + AND (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = P.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = P.NUMNOTA) = 0 + AND P.CONDVENDA = 10 + AND P.POSICAO = 'F' THEN 'Em Trânsito' + WHEN P.POSICAO = 'M' + AND P.CONDVENDA = 10 THEN 'Em Separação' + WHEN P.POSICAO IN ( 'L', 'P' ) + AND P.CONDVENDA = 10 THEN 'Aguardando Separação' + WHEN P.CONDVENDA NOT IN ( 10 ) THEN NULL + ELSE 'Concluída' END as "statusTransfer" +FROM SEVEN.ESTLOGTRANSFCD E +LEFT JOIN PCPEDC P ON (E.NUMPEDTRANSF IS NOT NULL AND E.NUMPEDTRANSF = P.NUMPED) + OR (E.NUMPEDTRANSF IS NULL AND E.NUMPEDLOJA = P.NUMPED) +WHERE 1=1 + `; + + const parameters: any[] = []; + let paramIndex = 0; + + if (filters?.dttransf) { + sql += ` AND DTTRANSF = TO_DATE(:dttransf, 'YYYY-MM-DD')`; + parameters.push(filters.dttransf); + paramIndex++; + } + + if (filters?.dttransfIni && filters?.dttransfEnd) { + sql += ` AND DTTRANSF BETWEEN TO_DATE(:dttransfIni, 'YYYY-MM-DD') AND TO_DATE(:dttransfEnd, 'YYYY-MM-DD')`; + parameters.push(filters.dttransfIni); + parameters.push(filters.dttransfEnd); + paramIndex += 2; + } else if (filters?.dttransfIni) { + sql += ` AND DTTRANSF >= TO_DATE(:dttransfIni, 'YYYY-MM-DD')`; + parameters.push(filters.dttransfIni); + paramIndex++; + } else if (filters?.dttransfEnd) { + sql += ` AND DTTRANSF <= TO_DATE(:dttransfEnd, 'YYYY-MM-DD')`; + parameters.push(filters.dttransfEnd); + paramIndex++; + } + + if (filters?.codfilial) { + sql += ` AND CODFILIAL = :codfilial`; + parameters.push(filters.codfilial); + paramIndex++; + } + + if (filters?.codfilialdest) { + sql += ` AND CODFILIALDEST = :codfilialdest`; + parameters.push(filters.codfilialdest); + paramIndex++; + } + + if (filters?.numpedloja) { + sql += ` AND NUMPEDLOJA = :numpedloja`; + parameters.push(filters.numpedloja); + paramIndex++; + } + + if (filters?.numpedtransf) { + sql += ` AND NUMPEDTRANSF = :numpedtransf`; + parameters.push(filters.numpedtransf); + paramIndex++; + } + + sql += ` ORDER BY DTTRANSF ASC`; + + const result = await this.oracleDataSource.query(sql, parameters); + return result.length > 0 ? result : null; + } + + + /** + * Retorna dados de um pedido específico cruzados com seu fechamento de caixa, + * filtrados apenas pelo número do pedido. Retorna apenas um registro. + */ + + async findorderbymark(orderId: number): Promise { + const sql = ` + SELECT p.MARCA, p.CODMARCA, p.ATIVO + FROM PCMARCA p + WHERE p.CODMARCA = :orderId + `; + + const results = await this.oracleDataSource.query(sql, [orderId]); + return results[0] || null; + } + + + async findOrderWithCheckoutByOrder(orderId: number): Promise { + const sql = ` +SELECT + e1.NOME AS NOME_FUNCIONARIO_CAIXA, + r.DTCXMOTHHMMSS AS DATA_FECHAMENTO, + e2.NOME AS NOME_ACERTO_MOTORISTA, + r.NUMCHECKOUT AS CAIXA +FROM + PCPREST r, + PCEMPR e1, + PCEMPR e2, + PCPEDC p +WHERE + p.NUMPED = r.NUMPED + AND e1.MATRICULA = r.CODFUNCCHECKOUT + AND e2.MATRICULA = r.CODFUNCCXMOT + AND p.NUMPED = :1 +`; + + const results = await this.oracleDataSource.query(sql, [orderId]); + return results[0] || null; + } + + + + async findOrders(query: FindOrdersDto): Promise { + const queryRunner = this.oracleDataSource.createQueryRunner(); + await queryRunner.connect(); try { let sql = `SELECT PCPEDC.DATA as "createDate" - ,PCPEDC.CODFILIAL || CASE WHEN PCPEDC.CODFILIALLOJA IS NOT NULL THEN - ' - Pre-Box ('||PCPEDC.CODFILIALLOJA||')' - ELSE NULL END as "storeId" - ,PCPEDC.NUMPED as "orderId" - ,PCPEDC.CODCLI as "customerId" - ,PCPEDC.CODCLI||' - '||PCCLIENT.CLIENTE as "customerName" - ,PCPEDC.CODUSUR as "sellerId" - ,PCPEDC.CODUSUR||' - '||PCUSUARI.NOME as "sellerName" - ,PCSUPERV.NOME as "store" - ,( SELECT CASE WHEN PCPEDI.TIPOENTREGA = 'EN' THEN 'Entrega ('||PCPEDI.TIPOENTREGA||')' - WHEN PCPEDI.TIPOENTREGA = 'EF' THEN 'Entrega Futura ('||PCPEDI.TIPOENTREGA||')' - WHEN PCPEDI.TIPOENTREGA = 'RP' THEN 'Retira Posterior ('||PCPEDI.TIPOENTREGA||')' - WHEN PCPEDI.TIPOENTREGA = 'RI' THEN 'Retira Imediata ('||PCPEDI.TIPOENTREGA||')' - ELSE 'Não Informado' END - FROM PCPEDI - WHERE PCPEDI.NUMPED = PCPEDC.NUMPED - AND ROWNUM = 1 ) as "deliveryType" - ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN - ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA - FROM PCPRACA - WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA ) - ELSE ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA - FROM PCCLIENTENDENT, PCPRACA - WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI - AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI - AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA ) END as "deliveryLocal" - ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN - ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO - FROM PCPRACA, PCROTAEXP - WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA - AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) - ELSE ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO - FROM PCCLIENTENDENT, PCPRACA, PCROTAEXP - WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI - AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI - AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA - AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) END as "masterDeliveryLocal" - ,CASE WHEN PCPEDC.CONDVENDA = 1 THEN 'TV1 - Retira Imediata' - WHEN PCPEDC.CONDVENDA = 7 THEN 'TV7 - Faturamento' - WHEN PCPEDC.CONDVENDA = 8 THEN 'TV8 - Entrega (' || - ( SELECT PCPEDI.TIPOENTREGA FROM PCPEDI - WHERE PCPEDI.NUMPED = PCPEDC.NUMPED - AND ROWNUM = 1 ) ||')' - WHEN PCPEDC.CONDVENDA = 10 THEN 'TV10 - Transferência' - ELSE 'Outros' END as "orderType" - ,CASE WHEN PCPEDC.POSICAO = 'P' THEN 'Pendente' - WHEN PCPEDC.POSICAO = 'B' THEN 'Bloqueado' - WHEN PCPEDC.POSICAO = 'L' THEN 'Liberado' - WHEN PCPEDC.POSICAO = 'M' THEN 'Montado' - WHEN PCPEDC.POSICAO = 'F' THEN 'Faturado' - WHEN PCPEDC.POSICAO = 'C' THEN 'Cancelado' - END as "status" - ,CASE WHEN PCPEDC.CONDVENDA IN (1,7) THEN PCPEDC.VLATEND ELSE PCPEDC.VLTOTAL END as "amount" - ,PCPEDC.DTENTREGA as "deliveryDate" - ,CASE WHEN PCPEDC.tipoprioridadeentrega = 'B' THEN 'Baixa' - WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'M' THEN 'Média' - WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'A' THEN 'Alta' - ELSE 'Não Definido' END as "deliveryPriority" - ,PCPEDC.NUMCAR as "shipmentId" - ,PCPEDC.DTLIBERA as "releaseDate" - ,PCPEDC.CODFUNCLIBERA as "releaseUser" - ,PCPEDC.CODFUNCLIBERA||'-'|| - (SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = PCPEDC.CODFUNCLIBERA) as "releaseUserName" - ,PCCARREG.DTSAIDA as "shipmentDate" - ,PCCARREG.DATAMON as "shipmentDateCreate" - ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL - ELSE PCCARREG.DTFECHA END as "shipmentCloseDate" - ,PCPEDC.CODPLPAG as "paymentId" - ,PCPLPAG.DESCRICAO as "paymentName" - ,PCPEDC.CODCOB as "billingId" - ,PCPEDC.CODCOB||'-'|| - PCCOB.COBRANCA as "billingName" - ,PCPEDC.DTFAT as "invoiceDate" - ,PCPEDC.HORAFAT as "invoiceHour" - ,PCPEDC.MINUTOFAT as "invoiceMinute" - ,PCPEDC.NUMNOTA as "invoiceNumber" - ,PCPEDC.MOTIVOPOSICAO as "BloqDescription" - ,PCNFSAID.DTCANHOTO as "confirmDeliveryDate" - ,PCPEDC.TOTPESO as "totalWeigth" - ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 4 - WHEN PCPEDC.POSICAO = 'F' THEN 3 - WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 2 - WHEN PCPEDC.POSICAO = 'M' THEN 1 - WHEN PCPEDC.POSICAO IN ('L', 'P', 'B') THEN 0 - ELSE 0 END as "processOrder" - ,( SELECT COUNT(1) FROM PCPREST, PCNFSAID, PCPEDC PED_VGER - WHERE PCPREST.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA - AND PCNFSAID.NUMTRANSVENDA = PED_VGER.NUMTRANSVENDA - AND PED_VGER.NUMPED = PCPEDC.NUMPEDENTFUT - AND PCPREST.DTPAG IS NULL - AND PCPREST.CODCOB = 'VGER' ) as "payment" - ,MOTORISTA.MATRICULA || ' - ' || MOTORISTA.NOME as "driver" - ,PCPEDC.NUMPEDENTFUT as "orderSaleId" - ,PCVEICUL.DESCRICAO||' ( '|| PCVEICUL.PLACA||' )' as "carDescription" - ,PCVEICUL.PLACA as "carIdentification" - ,PCPEDC.CODFORNECFRETE||' - '||PCFORNEC.FORNECEDOR as "carrier" - ,CASE WHEN (SELECT COUNT(1) FROM PCNFENT, PCFILIAL - WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL - AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC - AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA ) = 0 - AND PCPEDC.CONDVENDA = 10 - AND PCPEDC.POSICAO = 'F' THEN 'Em Trânsito' - WHEN PCPEDC.POSICAO = 'M' - AND PCPEDC.CONDVENDA = 10 THEN 'Em Separação' - WHEN PCPEDC.POSICAO IN ( 'L', 'P' ) - AND PCPEDC.CONDVENDA = 10 THEN 'Aguardando Separação' - WHEN PCPEDC.CONDVENDA NOT IN ( 10 ) THEN NULL - ELSE 'Concluída' END as "statusTransfer" - ,PCPEDC.CODFILIALLOJA as "storePreBox" - FROM PCPEDC, PCCLIENT, PCUSUARI, PCSUPERV, PCCOB, PCPLPAG, PCCARREG, PCNFSAID, - PCEMPR MOTORISTA, PCVEICUL, PCFORNEC - WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI - AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR - AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG - AND PCPEDC.CODCOB = PCCOB.CODCOB - AND PCPEDC.CODSUPERVISOR = PCSUPERV.CODSUPERVISOR - AND PCPEDC.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA (+) - AND PCPEDC.NUMCAR = PCCARREG.NUMCAR (+) - AND PCCARREG.CODMOTORISTA = MOTORISTA.MATRICULA (+) - AND PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO (+) - AND PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC (+)`; + ,PCPEDC.CODFILIAL || CASE WHEN PCPEDC.CODFILIALLOJA IS NOT NULL THEN + ' - Pre-Box ('||PCPEDC.CODFILIALLOJA||')' + ELSE NULL END as "storeId" + ,PCPEDC.NUMPED as "orderId" + ,PCPEDC.CODCLI as "customerId" + ,PCPEDC.CODCLI||' - '||PCCLIENT.CLIENTE as "customerName" + ,PCPEDC.CODUSUR as "sellerId" + ,PCPEDC.CODUSUR3 as "partnerId" + ,PCPEDC.HORAFAT as "HORA FATURAMENTO" + ,PCPEDC.MINUTOFAT as "MINUTO FATURAMENTO" + ,ESTPARCEIRO.NOME as "partnerName" + ,PCPEDC.CODUSUR||' - '||PCUSUARI.NOME as "sellerName" + ,PCSUPERV.NOME as "store" + ,( SELECT CASE WHEN PCPEDI.TIPOENTREGA = 'EN' THEN 'Entrega ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'EF' THEN 'Entrega Futura ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'RP' THEN 'Retira Posterior ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'RI' THEN 'Retira Imediata ('||PCPEDI.TIPOENTREGA||')' + ELSE 'Não Informado' END + FROM PCPEDI + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED + AND ROWNUM = 1 ) as "deliveryType" + ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN + ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA + FROM PCPRACA + WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA ) + ELSE ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA + FROM PCCLIENTENDENT, PCPRACA + WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI + AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI + AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA ) END as "deliveryLocal" + ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN + ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO + FROM PCPRACA, PCROTAEXP + WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA + AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) + ELSE ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO + FROM PCCLIENTENDENT, PCPRACA, PCROTAEXP + WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI + AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI + AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA + AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) END as "masterDeliveryLocal" + ,CASE WHEN PCPEDC.CONDVENDA = 1 THEN 'TV1 - Retira Imediata' + WHEN PCPEDC.CONDVENDA = 7 THEN 'TV7 - Faturamento' + WHEN PCPEDC.CONDVENDA = 8 THEN 'TV8 - Entrega (' || + ( SELECT PCPEDI.TIPOENTREGA FROM PCPEDI + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED + AND ROWNUM = 1 ) ||')' + WHEN PCPEDC.CONDVENDA = 10 THEN 'TV10 - Transferência' + ELSE 'Outros' END as "orderType" + ,CASE WHEN PCPEDC.CONDVENDA IN (1,7) THEN PCPEDC.VLATEND ELSE PCPEDC.VLTOTAL END as "amount" + ,PCPEDC.DTENTREGA as "deliveryDate" + ,CASE WHEN PCPEDC.tipoprioridadeentrega = 'B' THEN 'Baixa' + WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'M' THEN 'Média' + WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'A' THEN 'Alta' + ELSE 'Não Definido' END as "deliveryPriority" + ,PCPEDC.NUMCAR as "shipmentId" + ,PCPEDC.DTLIBERA as "releaseDate" + ,PCPEDC.CODFUNCLIBERA as "releaseUser" + ,PCPEDC.CODFUNCLIBERA||'-'|| + (SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = PCPEDC.CODFUNCLIBERA) as "releaseUserName" + ,PCCARREG.DTSAIDA as "shipmentDate" + ,PCCARREG.DATAMON as "shipmentDateCreate" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL + ELSE PCCARREG.DTFECHA END as "shipmentCloseDate" + ,PCPEDC.CODPLPAG as "paymentId" + ,PCPLPAG.DESCRICAO as "paymentName" + ,PCPEDC.CODCOB as "billingId" + ,PCPEDC.CODCOB||'-'|| + PCCOB.COBRANCA as "billingName" + ,PCPEDC.DTFAT as "invoiceDate" + ,PCPEDC.HORAFAT as "invoiceHour" + ,PCPEDC.MINUTOFAT as "invoiceMinute" + ,PCPEDC.NUMNOTA as "invoiceNumber" + ,PCPEDC.MOTIVOPOSICAO as "BloqDescription" + ,PCNFSAID.DTCANHOTO as "confirmDeliveryDate" + ,PCPEDC.TOTPESO as "totalWeigth" + ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 4 + WHEN PCPEDC.POSICAO = 'F' THEN 3 + WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 2 + WHEN PCPEDC.POSICAO = 'M' THEN 1 + WHEN PCPEDC.POSICAO IN ('L', 'P', 'B') THEN 0 + ELSE 0 END as "processOrder" + ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 'ENTREGA CONFIRMADA' + WHEN PCPEDC.POSICAO = 'F' THEN 'FATURADO' + WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 'SEPARADO' + WHEN PCPEDC.POSICAO = 'M' THEN 'MONTADO' + WHEN PCPEDC.POSICAO = 'C' THEN 'CANCELADO' + WHEN PCPEDC.POSICAO = 'L' THEN 'AGUARDANDO SEPARAÇÃO' + WHEN PCPEDC.POSICAO = 'P' THEN 'ESTOQUE PENDENTE' + WHEN PCPEDC.POSICAO = 'B' THEN 'BLOQUEIO FINANCEIRO' + ELSE 'NÃO DEFINIDO' END as "status" + ,( SELECT COUNT(1) FROM PCPREST, PCNFSAID, PCPEDC PED_VGER + WHERE PCPREST.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA + AND PCNFSAID.NUMTRANSVENDA = PED_VGER.NUMTRANSVENDA + AND PED_VGER.NUMPED = PCPEDC.NUMPEDENTFUT + AND PCPREST.DTPAG IS NULL + AND PCPREST.CODCOB = 'VGER' ) as "payment" + ,MOTORISTA.MATRICULA || ' - ' || MOTORISTA.NOME as "driver" + ,PCPEDC.NUMPEDENTFUT as "orderSaleId" + ,PCVEICUL.DESCRICAO||' ( '|| PCVEICUL.PLACA||' )' as "carDescription" + ,PCVEICUL.PLACA as "carIdentification" + ,PCPEDC.CODFORNECFRETE||' - '||PCFORNEC.FORNECEDOR as "carrier" + ,CASE WHEN (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA ) = 0 + AND PCPEDC.CONDVENDA = 10 + AND PCPEDC.POSICAO = 'F' THEN 'Em Trânsito' + WHEN PCPEDC.POSICAO = 'M' + AND PCPEDC.CONDVENDA = 10 THEN 'Em Separação' + WHEN PCPEDC.POSICAO IN ( 'L', 'P' ) + AND PCPEDC.CONDVENDA = 10 THEN 'Aguardando Separação' + WHEN PCPEDC.CONDVENDA NOT IN ( 10 ) THEN NULL + ELSE 'CONCLUIDA' END as "statusTransfer" + ,PCPEDC.CODFILIALLOJA as "storePreBox" + ,(SELECT MAX(EST.NUMPEDTRANSF) FROM SEVEN.ESTLOGTRANSFCD EST WHERE EST.NUMPEDLOJA = PCPEDC.NUMPED) as "transferOrderNumber" + ,CASE WHEN NVL(PCPEDCTEMP.DTENTREGAORIG, PCPEDC.DTENTREGA) < PCPEDC.DTENTREGA THEN 'ENTREGA AGENDADA' + ELSE 'ENTREGA NORMAL' END as "schedulerDelivery" + ,NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT)) as "fatUserCode" + ,(SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT))) as "fatUserName" + ,CASE WHEN NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT)) IS NOT NULL + AND NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT)) != -1 THEN + NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT)) || '-' || + (SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = NVL(PCNFSAID.CODEMITENTE, DECODE(PCPEDC.NUMCAR,0,-1,PCCARREG.CODFUNCFAT))) + ELSE NULL END as "fatUserDescription" + ,PCPEDC.CODEMITENTE as "codEmitente" + ,PCPEDC.CODEMITENTE as "emitenteMatricula" + ,(SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = PCPEDC.CODEMITENTE) as "emitenteNome" + FROM PCPEDC, PCCLIENT, PCUSUARI, PCSUPERV, PCCOB, PCPLPAG, PCCARREG, PCNFSAID, + PCEMPR MOTORISTA, PCVEICUL, PCFORNEC, PCPEDCTEMP, PCPEDI, PCPRODUT, PCMARCA, ESTPARCEIRO + WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI + AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR + AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG + AND PCPEDC.CODCOB = PCCOB.CODCOB + AND PCPEDC.NUMPED = PCPEDCTEMP.NUMPED (+) + AND PCPEDC.CODSUPERVISOR = PCSUPERV.CODSUPERVISOR + AND PCPEDC.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA (+) + AND PCPEDC.NUMCAR = PCCARREG.NUMCAR (+) + AND PCCARREG.CODMOTORISTA = MOTORISTA.MATRICULA (+) + AND PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO (+) + AND PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC (+) + AND PCPEDI.NUMPED = PCPEDC.NUMPED + AND PCPEDI.CODPROD = PCPRODUT.CODPROD + AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+) + AND PCPEDC.CODUSUR3 = ESTPARCEIRO.ID (+)`; const conditions: string[] = []; @@ -162,9 +406,14 @@ export class OrdersRepository { if (query.customerId) { conditions.push(`AND PCPEDC.CODCLI = :customerId`); } + + if (query.billingId) { conditions.push(`AND PCPEDC.CODCOB = :billingId`); } + if (query.partnerId) { + conditions.push(`AND PCPEDC.CODUSUR3 = :partnerId`); + } if (query.orderId) { conditions.push( `AND (PCPEDC.NUMPED = :orderId OR PCPEDC.NUMPEDENTFUT = :orderId)` @@ -201,6 +450,48 @@ export class OrdersRepository { if (query.shippimentId) { conditions.push(`AND PCPEDC.NUMCAR = :shippimentId`); } + if (query.carrier) { + conditions.push(`AND PCPEDC.CODFORNECFRETE = :carrier`); + } + if (query.markId) { + conditions.push(`AND PCMARCA.CODMARCA = :markId`); + } + if (query.markName) { + conditions.push(`AND UPPER(PCMARCA.MARCA) LIKE UPPER('%' || :markName || '%')`); + } + if (query.hasPreBox === true) { + conditions.push(`AND EXISTS (SELECT 1 FROM SEVEN.ESTLOGTRANSFCD WHERE NUMPEDLOJA = PCPEDC.NUMPED)`); + } + + if (query.preBoxFilial) { + conditions.push(`AND PCPEDC.CODFILIALLOJA = :preBoxFilial`); + } + + if (query.transferDestFilial) { + conditions.push(`AND PCPEDC.CODFILIAL = :transferDestFilial`); + } + + if (query.transferDateIni && query.transferDateEnd) { + conditions.push(`AND EXISTS ( + SELECT 1 FROM SEVEN.ESTLOGTRANSFCD + WHERE NUMPEDLOJA = PCPEDC.NUMPED + AND DTTRANSF BETWEEN TO_DATE(:transferDateIni, 'YYYY-MM-DD') + AND TO_DATE(:transferDateEnd, 'YYYY-MM-DD') + )`); + } else if (query.transferDateIni) { + conditions.push(`AND EXISTS ( + SELECT 1 FROM SEVEN.ESTLOGTRANSFCD + WHERE NUMPEDLOJA = PCPEDC.NUMPED + AND DTTRANSF >= TO_DATE(:transferDateIni, 'YYYY-MM-DD') + )`); + } else if (query.transferDateEnd) { + conditions.push(`AND EXISTS ( + SELECT 1 FROM SEVEN.ESTLOGTRANSFCD + WHERE NUMPEDLOJA = PCPEDC.NUMPED + AND DTTRANSF <= TO_DATE(:transferDateEnd, 'YYYY-MM-DD') + )`); + } + if (query.deliveryType) { const types = query.deliveryType .split(",") @@ -224,32 +515,408 @@ export class OrdersRepository { .join(","); conditions.push(`AND PCPEDC.CONDVENDA IN (${types})`); } - if (query.onlyPendentingTransfer === "S") { + if (query.onlyPendingTransfer === true) { conditions.push(` - AND NOT EXISTS(SELECT 1 FROM PCNFENT, PCFILIAL + AND NOT EXISTS(SELECT 1 FROM PCNFENT, PCFILIAL WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA)`); } - sql += "\n" + conditions.join("\n"); - sql += "\nAND ROWNUM < 5000"; + if (query.statusTransfer) { + const statusTransferList = query.statusTransfer + .split(",") + .map((s) => s.trim()); - const orders = await queryRunner.manager.query(sql); + const statusConditions = statusTransferList.map(status => { + switch (status) { + case 'Em Trânsito': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO = 'F' AND + (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA) = 0)`; + case 'Em Separação': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO = 'M')`; + case 'Aguardando Separação': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO IN ('L', 'P'))`; + case 'Concluída': + return `(PCPEDC.CONDVENDA = 10 AND + (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA) > 0)`; + default: + return null; + } + }).filter(condition => condition !== null); + + if (statusConditions.length > 0) { + conditions.push(`AND (${statusConditions.join(' OR ')})`); + } + } + + sql += "\n" + conditions.join("\n"); + sql += "\nGROUP BY PCPEDC.DATA, PCPEDC.CODFILIAL, PCPEDC.CODFILIALLOJA, PCPEDC.NUMPED, PCPEDC.CODCLI, PCPEDC.CODENDENTCLI, PCPEDC.CODPRACA, PCPEDC.CODUSUR, PCPEDC.CODUSUR3, PCPEDC.CODSUPERVISOR, PCPEDC.CONDVENDA, PCPEDC.VLATEND, PCPEDC.VLTOTAL, PCPEDC.DTENTREGA, PCPEDC.TIPOPRIORIDADEENTREGA, PCPEDC.NUMCAR, PCPEDC.DTLIBERA, PCPEDC.CODFUNCLIBERA, PCPEDC.NUMTRANSVENDA, PCPEDC.CODPLPAG, PCPEDC.CODCOB, PCPEDC.DTFAT, PCPEDC.HORAFAT, PCPEDC.MINUTOFAT, PCPEDC.NUMNOTA, PCPEDC.MOTIVOPOSICAO, PCPEDC.TOTPESO, PCPEDC.POSICAO, PCPEDC.DTFINALSEP, PCPEDC.NUMPEDENTFUT, PCPEDC.CODFORNECFRETE, PCPEDC.CODEMITENTE, PCCLIENT.CLIENTE, PCUSUARI.NOME, PCSUPERV.NOME, PCCARREG.DTSAIDA, PCCARREG.DATAMON, PCCARREG.DTFECHA, PCCARREG.CODFUNCFAT, PCNFSAID.CODEMITENTE, PCPLPAG.DESCRICAO, PCCOB.COBRANCA, PCNFSAID.DTCANHOTO, MOTORISTA.MATRICULA, MOTORISTA.NOME, PCVEICUL.DESCRICAO, PCVEICUL.PLACA, PCFORNEC.FORNECEDOR, PCPEDCTEMP.DTENTREGAORIG, ESTPARCEIRO.NOME"; + sql += "\nORDER BY PCPEDC.NUMPED DESC"; + sql += "\nFETCH FIRST 5000 ROWS ONLY"; + + const parameters: any = {}; + + // Add parameters for bind variables + if (query.codfilial) { + parameters.storeId = query.codfilial; + } + if (query.filialretira) { + parameters.storeStockId = query.filialretira; + } + if (query.sellerId) { + parameters.sellerId = query.sellerId; + } + if (query.customerId) { + parameters.customerId = query.customerId; + } + if (query.billingId) { + parameters.billingId = query.billingId; + } + if (query.partnerId) { + parameters.partnerId = query.partnerId; + } + if (query.orderId) { + parameters.orderId = query.orderId; + } + if (query.invoiceId) { + parameters.invoiceId = query.invoiceId; + } + if (query.hour) { + parameters.hour = query.hour; + } + if (query.minute) { + parameters.minute = query.minute; + } + + if (query.productId) { + parameters.productId = query.productId; + } + if (query.createDateIni) { + parameters.createDateIni = query.createDateIni; + } + if (query.createDateEnd) { + parameters.createDateEnd = query.createDateEnd; + } + if (query.invoiceDateIni) { + parameters.invoiceDateIni = query.invoiceDateIni; + } + if (query.invoiceDateEnd) { + parameters.invoiceDateEnd = query.invoiceDateEnd; + } + if (query.shippimentId) { + parameters.shippimentId = query.shippimentId; + } + if (query.carrier) { + parameters.carrier = query.carrier; + } + if (query.markId) { + parameters.markId = query.markId; + } + if (query.markName) { + parameters.markName = query.markName; + } + + // Novos parâmetros para filtros de transferência + if (query.preBoxFilial) { + parameters.preBoxFilial = query.preBoxFilial; + } + + if (query.transferDestFilial) { + parameters.transferDestFilial = query.transferDestFilial; + } + + if (query.transferDateIni) { + parameters.transferDateIni = query.transferDateIni; + } + + if (query.transferDateEnd) { + parameters.transferDateEnd = query.transferDateEnd; + } + + const orders = await queryRunner.manager.query(sql, parameters); + return orders; + } finally { + await queryRunner.release(); + } + } + + /** + * Busca pedidos por data de entrega com filtros específicos + */ + async findOrdersByDeliveryDate(query: any): Promise { + const queryRunner = this.oracleDataSource.createQueryRunner(); + await queryRunner.connect(); + try { + let sql = `SELECT PCPEDC.DATA as "createDate" + ,PCPEDC.CODFILIAL || CASE WHEN PCPEDC.CODFILIALLOJA IS NOT NULL THEN + ' - Pre-Box ('||PCPEDC.CODFILIALLOJA||')' + ELSE NULL END as "storeId" + ,PCPEDC.NUMPED as "orderId" + ,PCPEDC.CODCLI as "customerId" + ,PCPEDC.CODCLI||' - '||PCCLIENT.CLIENTE as "customerName" + ,PCPEDC.CODUSUR as "sellerId" + ,PCPEDC.CODUSUR||' - '||PCUSUARI.NOME as "sellerName" + ,PCSUPERV.NOME as "store" + ,( SELECT CASE WHEN PCPEDI.TIPOENTREGA = 'EN' THEN 'Entrega ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'EF' THEN 'Entrega Futura ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'RP' THEN 'Retira Posterior ('||PCPEDI.TIPOENTREGA||')' + WHEN PCPEDI.TIPOENTREGA = 'RI' THEN 'Retira Imediata ('||PCPEDI.TIPOENTREGA||')' + ELSE 'Não Informado' END + FROM PCPEDI + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED + AND ROWNUM = 1 ) as "deliveryType" + ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN + ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA + FROM PCPRACA + WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA ) + ELSE ( SELECT PCPRACA.CODPRACA||'-'||PCPRACA.PRACA + FROM PCCLIENTENDENT, PCPRACA + WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI + AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI + AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA ) END as "deliveryLocal" + ,CASE WHEN NVL(PCPEDC.CODENDENTCLI,0) = 0 THEN + ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO + FROM PCPRACA, PCROTAEXP + WHERE PCPRACA.CODPRACA = PCPEDC.CODPRACA + AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) + ELSE ( SELECT PCROTAEXP.CODROTA||'-'||PCROTAEXP.DESCRICAO + FROM PCCLIENTENDENT, PCPRACA, PCROTAEXP + WHERE PCCLIENTENDENT.CODCLI = PCPEDC.CODCLI + AND PCCLIENTENDENT.CODENDENTCLI = PCPEDC.CODENDENTCLI + AND PCCLIENTENDENT.CODPRACAENT = PCPRACA.CODPRACA + AND PCPRACA.ROTA = PCROTAEXP.CODROTA ) END as "masterDeliveryLocal" + ,CASE WHEN PCPEDC.CONDVENDA = 1 THEN 'TV1 - Retira Imediata' + WHEN PCPEDC.CONDVENDA = 7 THEN 'TV7 - Faturamento' + WHEN PCPEDC.CONDVENDA = 8 THEN 'TV8 - Entrega (' || + ( SELECT PCPEDI.TIPOENTREGA FROM PCPEDI + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED + AND ROWNUM = 1 ) ||')' + WHEN PCPEDC.CONDVENDA = 10 THEN 'TV10 - Transferência' + ELSE 'Outros' END as "orderType" + ,CASE WHEN PCPEDC.CONDVENDA IN (1,7) THEN PCPEDC.VLATEND ELSE PCPEDC.VLTOTAL END as "amount" + ,PCPEDC.DTENTREGA as "deliveryDate" + ,CASE WHEN PCPEDC.tipoprioridadeentrega = 'B' THEN 'Baixa' + WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'M' THEN 'Média' + WHEN PCPEDC.TIPOPRIORIDADEENTREGA = 'A' THEN 'Alta' + ELSE 'Não Definido' END as "deliveryPriority" + ,PCPEDC.NUMCAR as "shipmentId" + ,PCPEDC.DTLIBERA as "releaseDate" + ,PCPEDC.CODFUNCLIBERA as "releaseUser" + ,PCPEDC.CODFUNCLIBERA||'-'|| + (SELECT PCEMPR.NOME FROM PCEMPR WHERE PCEMPR.MATRICULA = PCPEDC.CODFUNCLIBERA) as "releaseUserName" + ,PCCARREG.DTSAIDA as "shipmentDate" + ,PCCARREG.DATAMON as "shipmentDateCreate" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL + ELSE PCCARREG.DTFECHA END as "shipmentCloseDate" + ,PCPEDC.CODPLPAG as "paymentId" + ,PCPLPAG.DESCRICAO as "paymentName" + ,PCPEDC.CODCOB as "billingId" + ,PCPEDC.CODCOB||'-'|| + PCCOB.COBRANCA as "billingName" + ,PCPEDC.DTFAT as "invoiceDate" + ,PCPEDC.HORAFAT as "invoiceHour" + ,PCPEDC.MINUTOFAT as "invoiceMinute" + ,PCPEDC.NUMNOTA as "invoiceNumber" + ,PCPEDC.MOTIVOPOSICAO as "BloqDescription" + ,PCNFSAID.DTCANHOTO as "confirmDeliveryDate" + ,PCPEDC.TOTPESO as "totalWeigth" + ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 4 + WHEN PCPEDC.POSICAO = 'F' THEN 3 + WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 2 + WHEN PCPEDC.POSICAO = 'M' THEN 1 + WHEN PCPEDC.POSICAO IN ('L', 'P', 'B') THEN 0 + ELSE 0 END as "processOrder" + ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 'ENTREGA CONFIRMADA' + WHEN PCPEDC.POSICAO = 'F' THEN 'FATURADO' + WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 'SEPARADO' + WHEN PCPEDC.POSICAO = 'M' THEN 'MONTADO' + WHEN PCPEDC.POSICAO = 'L' THEN 'AGUARDANDO SEPARAÇÃO' + WHEN PCPEDC.POSICAO = 'P' THEN 'ESTOQUE PENDENTE' + WHEN PCPEDC.POSICAO = 'B' THEN 'BLOQUEIO FINANCEIRO' + ELSE 'NÃO DEFINIDO' END as "status" + ,( SELECT COUNT(1) FROM PCPREST, PCNFSAID, PCPEDC PED_VGER + WHERE PCPREST.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA + AND PCNFSAID.NUMTRANSVENDA = PED_VGER.NUMTRANSVENDA + AND PED_VGER.NUMPED = PCPEDC.NUMPEDENTFUT + AND PCPREST.DTPAG IS NULL + AND PCPREST.CODCOB = 'VGER' ) as "payment" + ,MOTORISTA.MATRICULA || ' - ' || MOTORISTA.NOME as "driver" + ,PCPEDC.NUMPEDENTFUT as "orderSaleId" + ,PCVEICUL.DESCRICAO||' ( '|| PCVEICUL.PLACA||' )' as "carDescription" + ,PCVEICUL.PLACA as "carIdentification" + ,PCPEDC.CODFORNECFRETE||' - '||PCFORNEC.FORNECEDOR as "carrier" + ,CASE WHEN (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA ) = 0 + AND PCPEDC.CONDVENDA = 10 + AND PCPEDC.POSICAO = 'F' THEN 'Em Trânsito' + WHEN PCPEDC.POSICAO = 'M' + AND PCPEDC.CONDVENDA = 10 THEN 'Em Separação' + WHEN PCPEDC.POSICAO IN ( 'L', 'P' ) + AND PCPEDC.CONDVENDA = 10 THEN 'Aguardando Separação' + WHEN PCPEDC.CONDVENDA NOT IN ( 10 ) THEN NULL + ELSE 'Concluída' END as "statusTransfer" + ,PCPEDC.CODFILIALLOJA as "storePreBox" + ,CASE WHEN NVL(PCPEDCTEMP.DTENTREGAORIG, PCPEDC.DTENTREGA) < PCPEDC.DTENTREGA THEN 'ENTREGA AGENDADA' + ELSE 'ENTREGA NORMAL' END as "schedulerDelivery" + FROM PCPEDC, PCCLIENT, PCUSUARI, PCSUPERV, PCCOB, PCPLPAG, PCCARREG, PCNFSAID, + PCEMPR MOTORISTA, PCVEICUL, PCFORNEC, PCPEDCTEMP, PCPEDI, PCPRODUT, PCMARCA + WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI + AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR + AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG + AND PCPEDC.CODCOB = PCCOB.CODCOB + AND NVL(NVL(PCPEDC.NUMPEDENTFUT, PCPEDC.NUMPEDTV1), PCPEDC.NUMPED) = PCPEDCTEMP.NUMPED (+) + AND PCPEDC.CODSUPERVISOR = PCSUPERV.CODSUPERVISOR + AND PCPEDC.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA (+) + AND PCPEDC.NUMCAR = PCCARREG.NUMCAR (+) + AND PCCARREG.CODMOTORISTA = MOTORISTA.MATRICULA (+) + AND PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO (+) + AND PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC (+) + AND PCPEDI.NUMPED = PCPEDC.NUMPED + AND PCPEDI.CODPROD = PCPRODUT.CODPROD + AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+)`; + + const conditions: string[] = []; + + // Filtros específicos para data de entrega + if (query.deliveryDateIni) { + conditions.push( + `AND PCPEDC.DTENTREGA >= TO_DATE(:deliveryDateIni, 'YYYY-MM-DD')` + ); + } + if (query.deliveryDateEnd) { + conditions.push( + `AND PCPEDC.DTENTREGA <= TO_DATE(:deliveryDateEnd, 'YYYY-MM-DD')` + ); + } + + // Filtros adicionais + if (query.codfilial) { + conditions.push(`AND PCPEDC.CODFILIAL = :storeId`); + } + if (query.sellerId) { + conditions.push(`AND PCPEDC.CODUSUR = :sellerId`); + } + if (query.customerId) { + conditions.push(`AND PCPEDC.CODCLI = :customerId`); + } + if (query.orderId) { + conditions.push( + `AND (PCPEDC.NUMPED = :orderId OR PCPEDC.NUMPEDENTFUT = :orderId)` + ); + } + if (query.deliveryType) { + const types = query.deliveryType + .split(",") + .map((t) => `'${t}'`) + .join(","); + conditions.push( + `AND EXISTS(SELECT 1 FROM PCPEDI WHERE PCPEDI.NUMPED = PCPEDC.NUMPED AND PCPEDI.TIPOENTREGA IN (${types}))` + ); + } + if (query.status) { + const statusList = query.status + .split(",") + .map((s) => `'${s}'`) + .join(","); + conditions.push(`AND PCPEDC.POSICAO IN (${statusList})`); + } + if (query.markId) { + conditions.push(`AND PCMARCA.CODMARCA = :markId`); + } + if (query.markName) { + conditions.push(`AND UPPER(PCMARCA.MARCA) LIKE UPPER('%' || :markName || '%')`); + } + if (query.hasPreBox === true) { + conditions.push(`AND EXISTS (SELECT 1 FROM SEVEN.ESTLOGTRANSFCD WHERE NUMPEDLOJA = PCPEDC.NUMPED)`); + } + + if (query.statusTransfer) { + const statusTransferList = query.statusTransfer + .split(",") + .map((s) => s.trim()); + + const statusConditions = statusTransferList.map(status => { + switch (status) { + case 'Em Trânsito': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO = 'F' AND + (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA) = 0)`; + case 'Em Separação': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO = 'M')`; + case 'Aguardando Separação': + return `(PCPEDC.CONDVENDA = 10 AND PCPEDC.POSICAO IN ('L', 'P'))`; + case 'Concluída': + return `(PCPEDC.CONDVENDA = 10 AND + (SELECT COUNT(1) FROM PCNFENT, PCFILIAL + WHERE PCFILIAL.CODIGO = PCPEDC.CODFILIAL + AND PCFILIAL.CODFORNEC = PCNFENT.CODFORNEC + AND PCNFENT.NUMNOTA = PCPEDC.NUMNOTA) > 0)`; + default: + return null; + } + }).filter(condition => condition !== null); + + if (statusConditions.length > 0) { + conditions.push(`AND (${statusConditions.join(' OR ')})`); + } + } + + sql += "\n" + conditions.join("\n"); + sql += "\nAND ROWNUM < 5000"; + + const parameters: any = {}; + + // Add parameters for bind variables + if (query.deliveryDateIni) { + parameters.deliveryDateIni = query.deliveryDateIni; + } + if (query.deliveryDateEnd) { + parameters.deliveryDateEnd = query.deliveryDateEnd; + } + if (query.codfilial) { + parameters.storeId = query.codfilial; + } + if (query.sellerId) { + parameters.sellerId = query.sellerId; + } + if (query.customerId) { + parameters.customerId = query.customerId; + } + if (query.orderId) { + parameters.orderId = query.orderId; + } + if (query.markId) { + parameters.markId = query.markId; + } + if (query.markName) { + parameters.markName = query.markName; + } + + const orders = await queryRunner.manager.query(sql, parameters); return orders; } finally { await queryRunner.release(); - await this.dataSource.destroy(); } } async findInvoice(chavenfe: string): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); try { const sql = ` - SELECT + SELECT pcnfsaid.codfilial AS "storeId", pcnfsaid.dtsaida AS "invoiceDate", pcnfsaid.numped AS "orderId", @@ -273,7 +940,7 @@ export class OrdersRepository { } const sqlItem = ` - SELECT + SELECT pcmov.codprod AS "productId", pcprodut.descricao AS "productName", pcprodut.embalagem AS "package", @@ -282,14 +949,14 @@ export class OrdersRepository { pcprodut.multiplo AS "multiple", pcprodut.tipoproduto AS "productType", REPLACE( - CASE - WHEN INSTR(pcprodut.URLIMAGEM, ';') > 0 + CASE + WHEN INSTR(pcprodut.URLIMAGEM, ';') > 0 THEN SUBSTR(pcprodut.URLIMAGEM,1,INSTR(pcprodut.URLIMAGEM, ';') - 1) - WHEN pcprodut.URLIMAGEM IS NOT NULL + WHEN pcprodut.URLIMAGEM IS NOT NULL THEN pcprodut.URLIMAGEM - ELSE NULL + ELSE NULL END, - '167.249.211.178:8001', + '167.249.211.178:8001', '10.1.1.191' ) AS "image" FROM pcmov @@ -307,19 +974,19 @@ export class OrdersRepository { } async getItens(orderId: string): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); try { const sql = `SELECT PCPEDI.CODPROD as "productId" , PCPRODUT.DESCRICAO as "description" , PCPRODUT.EMBALAGEM as "pacth" - , NVL(PCPEDI.COMPLEMENTO, + , NVL(PCPEDI.COMPLEMENTO, ( SELECT TV7I.COMPLEMENTO FROM PCPEDC, PCPEDC TV7, PCPEDITEMP TV7I WHERE TV7.NUMPED = PCPEDC.NUMPEDENTFUT - AND PCPEDC.NUMPED = PCPEDI.NUMPED + AND PCPEDC.NUMPED = PCPEDI.NUMPED AND TV7.NUMPEDRCA = TV7I.NUMPEDRCA AND TV7I.CODPROD = PCPEDI.codprod - AND TV7I.NUMSEQ = PCPEDI.NUMSEQ ) ) as "color" + AND TV7I.NUMSEQ = PCPEDI.NUMSEQ ) ) as "color" , PCPEDI.CODFILIALRETIRA as "stockId" , PCPEDI.QT as "quantity" , PCPEDI.PVENDA as "salePrice" @@ -328,7 +995,7 @@ export class OrdersRepository { WHEN PCPEDI.TIPOENTREGA = 'EN' THEN 'ENTREGA' WHEN PCPEDI.TIPOENTREGA = 'EF' THEN 'ENTREGA FUTURA' END as "deliveryType" - , ( PCPEDI.QT * + , ( PCPEDI.QT * PCPEDI.PVENDA ) as "total" , ( PCPEDI.QT * PCPRODUT.PESOBRUTO ) as "weigth" , PCDEPTO.DESCRICAO as "department" @@ -336,7 +1003,7 @@ export class OrdersRepository { FROM PCPEDI, PCPRODUT, PCDEPTO, PCMARCA WHERE PCPEDI.CODPROD = PCPRODUT.CODPROD AND PCPRODUT.CODEPTO = PCDEPTO.CODEPTO - AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA + AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA AND PCPEDI.NUMPED = ${orderId}`; const itens = await queryRunner.manager.query(sql); @@ -346,12 +1013,12 @@ export class OrdersRepository { } } async getCutItens(orderId: string): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); try { const sql = ` - SELECT + SELECT PCCORTEI.CODPROD as "productId", PCPRODUT.DESCRICAO as "description", PCPRODUT.EMBALAGEM as "pacth", @@ -372,9 +1039,9 @@ export class OrdersRepository { } async getOrderDelivery(orderId: string): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); - + try { const sql = ` SELECT PCPRACA.CODPRACA as "placeId", @@ -416,7 +1083,7 @@ export class OrdersRepository { AND PCPEDC.CODFUNCCONF = CONFERENTE.MATRICULA (+) AND PCPEDC.NUMPED = ${orderId} `; - + const result = await queryRunner.manager.query(sql); return result.length > 0 ? result[0] : null; } finally { @@ -424,12 +1091,12 @@ export class OrdersRepository { } } async getTransfer(orderId: number): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); - + try { const sql = ` - SELECT + SELECT L.NUMPED as "orderId", L.DTTRANSF as "transferDate", L.NUMNOTA as "invoiceId", @@ -447,7 +1114,7 @@ export class OrdersRepository { SELECT NUMTRANSVENDA FROM PCPEDC WHERE PCPEDC.NUMPED = ${orderId} ) `; - + const result = await queryRunner.manager.query(sql); return result.length > 0 ? result : null; } finally { @@ -456,9 +1123,9 @@ export class OrdersRepository { } async getStatusOrder(orderId: number): Promise { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); - + try { const sql = `SELECT pcpedc.numped AS "orderId", 'Digitação pedido' AS "status", @@ -466,7 +1133,7 @@ export class OrdersRepository { 'DD/MM/YYYY HH24:MI') AS "statusDate", pcpedc.codemitente || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr WHERE pcpedc.numped = ${orderId} AND pcpedc.codemitente = pcempr.matricula(+) UNION ALL @@ -481,7 +1148,7 @@ export class OrdersRepository { 'DD/MM/YYYY HH24:MI') AS "statusDate", pccarreg.codfuncmon || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr, pccarreg WHERE pcpedc.numped = ${orderId} AND pccarreg.codfuncmon = pcempr.matricula(+) @@ -501,7 +1168,7 @@ export class OrdersRepository { 'DD/MM/YYYY HH24:MI') ELSE NULL END AS "statusDate", pcpedc.codfuncemissaomapa || '-' || pcempr.nome AS "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr WHERE pcpedc.numped = ${orderId} AND pcpedc.codfuncemissaomapa = pcempr.matricula(+) @@ -511,9 +1178,9 @@ export class OrdersRepository { 'Inicio de Separação' AS "status", pcpedc.dtinicialsep as "statusDate", pcpedc.codfuncsep || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr - WHERE pcpedc.numped = ${orderId} + WHERE pcpedc.numped = ${orderId} AND pcpedc.codfuncsep = pcempr.matricula(+) AND PCPEDC.dtinicialsep IS NOT NULL UNION ALL @@ -521,7 +1188,7 @@ export class OrdersRepository { 'Fim de Separação' AS "status", pcpedc.dtfinalsep as "statusDate", pcpedc.codfuncsep || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr WHERE pcpedc.numped = ${orderId} AND pcpedc.codfuncsep = pcempr.matricula(+) and pcpedc.dtfinalsep is not null @@ -530,9 +1197,9 @@ export class OrdersRepository { 'Inicio conferência' AS "status", pcpedc.dtinicialcheckout AS "statusDate", pcpedc.codfuncconf || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr - WHERE pcpedc.numped = ${orderId} + WHERE pcpedc.numped = ${orderId} AND pcpedc.codfuncconf = pcempr.matricula(+) AND pcpedc.dtinicialcheckout IS NOT NULL UNION ALL @@ -540,9 +1207,9 @@ export class OrdersRepository { 'Fim conferência' AS "status", pcpedc.dtfinalcheckout AS "statusDate", pcpedc.codfuncconf || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr - WHERE pcpedc.numped = ${orderId} + WHERE pcpedc.numped = ${orderId} AND pcpedc.codfuncconf = pcempr.matricula(+) AND pcpedc.dtfinalcheckout IS NOT NULL UNION ALL @@ -553,7 +1220,7 @@ export class OrdersRepository { 'DD/MM/YYYY HH24:MI') AS "statusDate", pcempr.matricula || '-' || pcempr.nome "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcempr, pccarreg, pcnfsaid WHERE pcpedc.numped = ${orderId} AND pcpedc.numcar = pccarreg.numcar @@ -567,15 +1234,15 @@ export class OrdersRepository { CASE WHEN PCNFSAID.NUMCAR > 0 THEN pcnfsaid.codfunccanhoto || '-' || pcempr.nome ELSE '' END "userName" - ,NULL as "comments" + ,NULL as "comments" FROM pcpedc, pcnfsaid, pcempr, pccarreg WHERE pcpedc.numped = ${orderId} AND pcpedc.numtransvenda = pcnfsaid.numtransvenda AND pcnfsaid.numcar = pccarreg.numcar (+) AND pcnfsaid.codfunccanhoto = pcempr.matricula(+) - AND pcnfsaid.dtcanhoto IS NOT NULL + AND pcnfsaid.dtcanhoto IS NOT NULL UNION ALL - SELECT pcpedc.numped AS "orderId", + SELECT pcpedc.numped AS "orderId", 'Transf entre carregamento' AS "status", pclogtransfnfcarreg.dttransf AS "statusDate", pclogtransfnfcarreg.codfunctransf || '-' || pcempr.nome "userName", @@ -586,7 +1253,7 @@ export class OrdersRepository { AND pcpedc.numped = ${orderId} AND pclogtransfnfcarreg.codfunctransf = pcempr.matricula(+) ORDER BY 3`; - + const result = await queryRunner.manager.query(sql); return result.length > 0 ? result : null; } finally { @@ -594,15 +1261,15 @@ export class OrdersRepository { } } async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> { - const queryRunner = this.dataSource.createQueryRunner(); + const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - + try { const sequenceSql = 'SELECT ESSCONFERENCIANF.NEXTVAL as "id" FROM DUAL'; const result = await queryRunner.manager.query(sequenceSql); const checkId = result[0].id; - + const sqlMain = ` INSERT INTO ESTCONFERENCIANF ( ID, NUMTRANSVENDA, CODFILIAL, NUMNOTA, DTINICIO, DTFIM, CODFUNCCONF @@ -614,7 +1281,7 @@ export class OrdersRepository { ) `; await queryRunner.manager.query(sqlMain); - + for (const item of invoice.itens) { const sqlItem = ` INSERT INTO ESTCONFERENCIANFITENS ( @@ -626,7 +1293,7 @@ export class OrdersRepository { `; await queryRunner.manager.query(sqlItem); } - + await queryRunner.commitTransaction(); return { message: 'Conferência salva com sucesso!' }; } catch (error) { @@ -636,4 +1303,270 @@ export class OrdersRepository { await queryRunner.release(); } } + async getOrderDeliveries(orderId: string, query: any) { + const queryRunnerOracle = this.oracleDataSource.createQueryRunner(); + const queryRunnerPostgres = this.postgresDataSource.createQueryRunner(); + + await queryRunnerOracle.connect(); + await queryRunnerPostgres.connect(); + + try { + const sqlOracle = `SELECT PCPEDC.CODFILIAL as "storeId" + ,PCPEDC.DATA as "createDate" + ,PCPEDC.NUMPED as "orderId" + ,PCPEDC.NUMPEDENTFUT as "orderIdSale" + ,PCPEDC.DTENTREGA as "deliveryDate" + ,PCPEDC.CODCLI as "customerId" + ,PCCLIENT.CLIENTE as "customer" + ,( SELECT PCPEDI.TIPOENTREGA FROM PCPEDI + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED AND ROWNUM = 1 ) as "deliveryType" + ,PCPEDC.NUMITENS as "quantityItens" + ,CASE WHEN PCNFSAID.DTCANHOTO IS NOT NULL THEN 'ENTREGA CONFIRMADA' + WHEN PCPEDC.POSICAO = 'F' THEN 'FATURADO' + WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN 'SEPARADO' + WHEN PCPEDC.POSICAO = 'M' THEN 'MONTADO' + WHEN PCPEDC.POSICAO = 'L' THEN 'AGUARDANDO SEPARAÇÃO' + WHEN PCPEDC.POSICAO = 'P' THEN 'PENDENTE ESTOQUE' + WHEN PCPEDC.POSICAO = 'B' THEN 'BLOQUEIO FINANCEIRO' + ELSE 'Não Definido' END as "status" + ,PCPEDC.TOTPESO as "weight" + ,PCPEDC.NUMCAR as "shipmentId" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL ELSE PCCARREG.CODMOTORISTA END as "driverId" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL ELSE PCEMPR.NOME END as "driverName" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL ELSE PCVEICUL.PLACA END as "carPlate" + ,CASE WHEN PCPEDC.NUMCAR = 0 THEN NULL ELSE PCVEICUL.OBS2 END as "carIdentification" + ,PCPEDC.OBS as "observation" + ,PCNFSAID.DTCANHOTO as "deliveryConfirmationDate" + FROM PCPEDC, PCCLIENT, PCCARREG, PCVEICUL, PCEMPR, PCNFSAID + WHERE PCPEDC.NUMCAR = PCCARREG.NUMCAR + AND PCPEDC.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA (+) + AND PCPEDC.CODCLI = PCCLIENT.CODCLI + AND PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO (+) + AND PCCARREG.CODMOTORISTA = PCEMPR.MATRICULA (+) + AND ( PCPEDC.NUMPEDENTFUT = ${orderId} OR PCPEDC.NUMPEDORIGEM = ${orderId} )`; + + + const orders = await queryRunnerOracle.manager.query(sqlOracle); + + // Consulta no WMS (Postgres) - Modificada para buscar pelo número do pedido + const sqlWMS = ` + SELECT p.numero, + p.posicao, + CASE + WHEN p.posicao = 'F' THEN 'FATURADO' + WHEN p.posicao = 'C' THEN 'CONCLUIDO' + WHEN p.posicao = 'L' THEN 'LIBERADO' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_inicio_separacao IS NULL + ) AND p.posicao = 'M' THEN 'MONTADO' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_inicio_separacao IS NOT NULL + AND m2.data_fim_separacao IS NULL + ) AND p.posicao = 'M' THEN 'EM SEPARACAO' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_fim_separacao IS NOT NULL + AND m2.data_inicio_conferencia IS NULL + ) AND p.posicao = 'M' THEN 'SEPARACAO FINALIZADA' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_inicio_conferencia IS NOT NULL + AND m2.data_fim_conferencia IS NULL + ) AND p.posicao = 'M' THEN 'EM CONFERENCIA' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_fim_conferencia IS NOT NULL + AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) = 0 + ) AND p.posicao = 'M' THEN 'CONFERENCIA FINALIZADA' + WHEN EXISTS ( + SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero + AND m2.data_fim_conferencia IS NOT NULL + AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) > 0 + ) AND p.posicao = 'M' THEN 'EMBARCADO' + END as "situacaoPedido" + FROM pedido p + WHERE p.numero IN ( + SELECT DISTINCT m.numero_pedido + FROM movimentacao m + WHERE m.numero_pedido = $1::bigint + OR m.numero_pedido IN ( + SELECT CAST(o.orderId AS bigint) FROM UNNEST($2::text[]) o(orderId) + WHERE o.orderId ~ '^[0-9]+$' + ) + ) + `; + + // Criar array com os IDs de pedidos obtidos do Oracle + const orderIds = orders.map(o => o.orderId?.toString() || ''); + + // Converter orderId para número para evitar erro de tipo + const numericOrderId = parseInt(orderId, 10); + const ordersWMS = await queryRunnerPostgres.manager.query(sqlWMS, [numericOrderId, orderIds]); + + // Atualizar status baseado no WMS + for (const order of orders) { + const orderWMS = ordersWMS.find(o => Number(o.numero) === Number(order.orderId)); + if (orderWMS && !order.deliveryConfirmationDate) { + order.status = orderWMS.situacaoPedido; + } + } + + return orders; + } catch (error) { + console.error(error); + throw new HttpException('Erro ao buscar pedidos', HttpStatus.INTERNAL_SERVER_ERROR); + } finally { + await queryRunnerOracle.release(); + await queryRunnerPostgres.release(); + } + } + + + async getLeadtimeWMS(orderId: string): Promise { + const queryRunnerPostgres = this.postgresDataSource.createQueryRunner(); + const queryRunnerOracle = this.oracleDataSource.createQueryRunner(); + + await queryRunnerPostgres.connect(); + await queryRunnerOracle.connect(); + + try { + // Consulta no Oracle + const sqlOracle = ` + SELECT + D.ETAPA as "etapa", + D.DESCRICAO_ETAPA as "descricaoEtapa", + D.DATA as "data", + D.CODIGOFUNCIONARIO as "codigoFuncionario", + D.NOMEFUNCIONARIO as "nomeFuncionario", + D.NUMEROPEDIDO as "numeroPedido" + FROM ESVACOMPANHAMENTOPEDIDO D + WHERE D.NUMEROPEDIDO = ${orderId} + ORDER BY D.ETAPA + `; + const dataOracle = await queryRunnerOracle.manager.query(sqlOracle); + + // Consulta no Postgres + const sqlPostgres = ` + SELECT DADOS.ETAPA as "etapa", + DADOS.DESCRICAO_ETAPA as "descricaoEtapa", + DADOS.DATA as "data", + DADOS.CODIGO_FUNCIONARIO as "codigoFuncionario", + DADOS.NOME_FUNCIONARIO as "nomeFuncionario", + DADOS.NUMERO_PEDIDO as "numeroPedido" + FROM ( + SELECT 3 AS ETAPA, 'Inicio Separação' AS DESCRICAO_ETAPA, + MIN(m.data_inicio_separacao) AS DATA, + MAX(m.codigo_separador) AS CODIGO_FUNCIONARIO, + (SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)) AS NOME_FUNCIONARIO, + m.numero_pedido + FROM movimentacao m + WHERE m.data_inicio_separacao >= '2025-01-01' + AND m.numero_pedido > 0 + AND m.data_inicio_separacao IS NOT NULL + GROUP BY m.numero_pedido + + UNION ALL + + SELECT 4, 'Separado', MIN(m.data_fim_separacao), MAX(m.codigo_separador), + (SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)), m.numero_pedido + FROM movimentacao m + WHERE m.data_inicio_separacao >= '2025-01-01' + AND m.numero_pedido > 0 + AND m.data_fim_separacao IS NOT NULL + GROUP BY m.numero_pedido + + UNION ALL + + SELECT 5, 'Inicio Conferência', MIN(m.data_inicio_conferencia), MAX(m.codigo_conferente), + (SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido + FROM movimentacao m + WHERE m.data_inicio_conferencia IS NOT NULL + AND m.numero_pedido > 0 + GROUP BY m.numero_pedido + + UNION ALL + + SELECT 6, 'Fim Conferência', MIN(m.data_fim_conferencia), MAX(m.codigo_conferente), + (SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido + FROM movimentacao m + WHERE m.data_fim_conferencia IS NOT NULL + AND m.numero_pedido > 0 + GROUP BY m.numero_pedido + + UNION ALL + + SELECT 7, 'Embarcado', MAX(v.data_embarque), v.usuario_embarque_id, + (SELECT u.nome FROM usuario u WHERE u.id = v.usuario_embarque_id), m.numero_pedido + FROM movimentacao m + JOIN volume v ON m.numero_pedido = v.numero_pedido + WHERE v.data_embarque IS NOT NULL + AND m.numero_pedido > 0 + GROUP BY v.usuario_embarque_id, m.numero_pedido + ) DADOS + WHERE DADOS.numero_pedido = $1 + ORDER BY DADOS.numero_pedido, DADOS.ETAPA; + `; + const dataPostgres = await queryRunnerPostgres.manager.query(sqlPostgres, [orderId]); + + // Junta os dados Oracle + Postgres + const leadtime = [...dataOracle, ...dataPostgres]; + + // Ordena pela etapa (opcional, para garantir ordem) + return leadtime.sort((a, b) => a.etapa - b.etapa); + } catch (error) { + console.error(error); + throw new HttpException('Erro ao buscar dados de leadtime do WMS', HttpStatus.INTERNAL_SERVER_ERROR); + } finally { + await queryRunnerPostgres.release(); + await queryRunnerOracle.release(); + } + } + + /** + * Busca as transportadoras de um pedido específico + */ + async getOrderCarriers(orderId: number): Promise { + const sql = ` + SELECT DISTINCT + PCPEDC.CODFORNECFRETE as "carrierId", + PCFORNEC.FORNECEDOR as "carrierName", + PCPEDC.CODFORNECFRETE || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription" + FROM PCPEDC + LEFT JOIN PCFORNEC ON PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC + WHERE PCPEDC.NUMPED = :0 + AND PCPEDC.CODFORNECFRETE IS NOT NULL + AND PCPEDC.CODFORNECFRETE > 0 + ORDER BY PCPEDC.CODFORNECFRETE + `; + return await this.oracleDataSource.query(sql, [orderId]); + } + + /** + * Busca todas as marcas disponíveis + */ + async getAllMarks(): Promise { + const sql = ` + SELECT p.MARCA, p.CODMARCA, p.ATIVO + FROM PCMARCA p + WHERE p.ATIVO = 'S' + ORDER BY p.MARCA + `; + return await this.oracleDataSource.query(sql); + } + + /** + * Busca marcas por nome (busca parcial) + */ + async getMarksByName(markName: string): Promise { + const sql = ` + SELECT p.MARCA, p.CODMARCA, p.ATIVO + FROM PCMARCA p + WHERE p.ATIVO = 'S' + AND UPPER(p.MARCA) LIKE UPPER('%' || :markName || '%') + ORDER BY p.MARCA + `; + return await this.oracleDataSource.query(sql, [markName]); + } } \ No newline at end of file diff --git a/src/partners/dto/partner.dto.ts b/src/partners/dto/partner.dto.ts new file mode 100644 index 0000000..bf11232 --- /dev/null +++ b/src/partners/dto/partner.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PartnerDto { + @ApiProperty({ description: 'Identificador do parceiro' }) + id: string; + + @ApiProperty({ description: 'CPF do parceiro' }) + cpf: string; + + @ApiProperty({ description: 'Nome do parceiro' }) + nome: string; + + constructor(partial: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/partners/partners.controller.ts b/src/partners/partners.controller.ts new file mode 100644 index 0000000..c163104 --- /dev/null +++ b/src/partners/partners.controller.ts @@ -0,0 +1,48 @@ +import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { Controller, Get, Param } from '@nestjs/common'; +import { PartnersService } from './partners.service'; +import { PartnerDto } from './dto/partner.dto'; + +@ApiTags('Parceiros') +@Controller('api/v1/') +export class PartnersController { + + constructor(private readonly partnersService: PartnersService) {} + + @Get('parceiros/:filter') + @ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' }) + @ApiParam({ name: 'filter', description: 'Filtro de busca (ID, CPF ou nome)' }) + @ApiResponse({ + status: 200, + description: 'Lista de parceiros encontrados.', + type: PartnerDto, + isArray: true + }) + async findPartners(@Param('filter') filter: string): Promise { + return this.partnersService.findPartners(filter); + } + + @Get('parceiros') + @ApiOperation({ summary: 'Lista todos os parceiros' }) + @ApiResponse({ + status: 200, + description: 'Lista de todos os parceiros.', + type: PartnerDto, + isArray: true + }) + async getAllPartners(): Promise { + return this.partnersService.getAllPartners(); + } + + @Get('parceiros/id/:id') + @ApiOperation({ summary: 'Busca parceiro por ID específico' }) + @ApiParam({ name: 'id', description: 'ID do parceiro' }) + @ApiResponse({ + status: 200, + description: 'Parceiro encontrado.', + type: PartnerDto + }) + async getPartnerById(@Param('id') id: string): Promise { + return this.partnersService.getPartnerById(id); + } +} diff --git a/src/partners/partners.module.ts b/src/partners/partners.module.ts new file mode 100644 index 0000000..0f7fc18 --- /dev/null +++ b/src/partners/partners.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { PartnersController } from './partners.controller'; +import { PartnersService } from './partners.service'; +import { DatabaseModule } from '../core/database/database.module'; +import { RedisModule } from '../core/configs/cache/redis.module'; + +@Module({ + imports: [DatabaseModule, RedisModule], + controllers: [PartnersController], + providers: [PartnersService], + exports: [PartnersService], +}) +export class PartnersModule {} diff --git a/src/partners/partners.service.ts b/src/partners/partners.service.ts new file mode 100644 index 0000000..5efc8ce --- /dev/null +++ b/src/partners/partners.service.ts @@ -0,0 +1,168 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { QueryRunner, DataSource } from 'typeorm'; +import { DATA_SOURCE } from '../core/constants'; +import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider'; +import { IRedisClient } from '../core/configs/cache/IRedisClient'; +import { getOrSetCache } from '../shared/cache.util'; +import { PartnerDto } from './dto/partner.dto'; + +@Injectable() +export class PartnersService { + private readonly PARTNERS_TTL = 60 * 60 * 12; // 12 horas + private readonly PARTNERS_CACHE_KEY = 'parceiros:search'; + + constructor( + @Inject(DATA_SOURCE) + private readonly dataSource: DataSource, + @Inject(RedisClientToken) + private readonly redisClient: IRedisClient, + ) {} + + /** + * Buscar parceiros com cache otimizado + * @param filter - Filtro de busca (ID, CPF ou nome) + * @returns Array de parceiros encontrados + */ + async findPartners(filter: string): Promise { + const cacheKey = `${this.PARTNERS_CACHE_KEY}:${filter}`; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.PARTNERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + // Primeira tentativa: busca por ID do parceiro + let sql = `SELECT ESTPARCEIRO.ID as "id", + ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || + ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", + ESTPARCEIRO.CPF as "cpf" + FROM ESTPARCEIRO + WHERE ESTPARCEIRO.ID = REGEXP_REPLACE('${filter}', '[^0-9]', '') + ORDER BY ESTPARCEIRO.NOME`; + let partners = await queryRunner.manager.query(sql); + + // Segunda tentativa: busca por CPF se não encontrou por ID + if (partners.length === 0) { + sql = `SELECT ESTPARCEIRO.ID as "id", + ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || + ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", + ESTPARCEIRO.CPF as "cpf" + FROM ESTPARCEIRO + WHERE REGEXP_REPLACE(ESTPARCEIRO.CPF, '[^0-9]', '') = REGEXP_REPLACE('${filter}', '[^0-9]', '') + ORDER BY ESTPARCEIRO.NOME`; + partners = await queryRunner.manager.query(sql); + } + + // Terceira tentativa: busca por nome do parceiro se não encontrou por ID ou CPF + if (partners.length === 0) { + sql = `SELECT ESTPARCEIRO.ID as "id", + ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || + ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", + ESTPARCEIRO.CPF as "cpf" + FROM ESTPARCEIRO + WHERE ESTPARCEIRO.NOME LIKE '${filter.toUpperCase().replace('@', '%')}%' + ORDER BY ESTPARCEIRO.NOME`; + partners = await queryRunner.manager.query(sql); + } + + return partners.map(partner => new PartnerDto({ + id: partner.id, + cpf: partner.cpf, + nome: partner.name + })); + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Buscar todos os parceiros com cache + * @returns Array de todos os parceiros + */ + async getAllPartners(): Promise { + const cacheKey = 'parceiros:all'; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.PARTNERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + const sql = `SELECT ESTPARCEIRO.ID as "id", + ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || + ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", + ESTPARCEIRO.CPF as "cpf" + FROM ESTPARCEIRO + ORDER BY ESTPARCEIRO.NOME`; + + const partners = await queryRunner.manager.query(sql); + return partners.map(partner => new PartnerDto({ + id: partner.id, + cpf: partner.cpf, + nome: partner.name + })); + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Buscar parceiro por ID específico com cache + * @param partnerId - ID do parceiro + * @returns Parceiro encontrado ou null + */ + async getPartnerById(partnerId: string): Promise { + const cacheKey = `parceiros:id:${partnerId}`; + + return getOrSetCache( + this.redisClient, + cacheKey, + this.PARTNERS_TTL, + async () => { + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + const sql = `SELECT ESTPARCEIRO.ID as "id", + ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || + ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", + ESTPARCEIRO.CPF as "cpf" + FROM ESTPARCEIRO + WHERE ESTPARCEIRO.ID = '${partnerId}'`; + + const partners = await queryRunner.manager.query(sql); + return partners.length > 0 ? new PartnerDto({ + id: partners[0].id, + cpf: partners[0].cpf, + nome: partners[0].name + }) : null; + } finally { + await queryRunner.release(); + } + } + ); + } + + /** + * Limpar cache de parceiros (útil para invalidação) + * @param pattern - Padrão de chaves para limpar (opcional) + */ + async clearPartnersCache(pattern?: string) { + const cachePattern = pattern || 'parceiros:*'; + + // Nota: Esta funcionalidade requer implementação específica do Redis + // Por enquanto, mantemos a interface para futuras implementações + console.log(`Cache de parceiros seria limpo para o padrão: ${cachePattern}`); + } +} diff --git a/src/shared/date.util.ts b/src/shared/date.util.ts new file mode 100644 index 0000000..e113039 --- /dev/null +++ b/src/shared/date.util.ts @@ -0,0 +1,113 @@ +/** + * Utilitário para manipulação de datas no horário brasileiro + */ +export class DateUtil { + private static readonly BRAZIL_TIMEZONE = 'America/Sao_Paulo'; + + /** + * Obtém a data atual no horário brasileiro + * @returns Data atual no horário brasileiro + */ + static now(): Date { + return new Date(); + } + + /** + * Converte uma data para string no formato ISO com timezone brasileiro + * @param date Data a ser convertida + * @returns String da data no formato ISO com timezone brasileiro + */ + static toBrazilISOString(date: Date): string { + return date.toLocaleString('sv-SE', { + timeZone: this.BRAZIL_TIMEZONE, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }).replace(' ', 'T') + '.000Z'; + } + + /** + * Converte uma data para string formatada no horário brasileiro + * @param date Data a ser convertida + * @param format Formato desejado (padrão: 'dd/MM/yyyy HH:mm:ss') + * @returns String da data formatada no horário brasileiro + */ + static toBrazilString(date: Date, format: string = 'dd/MM/yyyy HH:mm:ss'): string { + const options: Intl.DateTimeFormatOptions = { + timeZone: this.BRAZIL_TIMEZONE, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }; + + const formatter = new Intl.DateTimeFormat('pt-BR', options); + const parts = formatter.formatToParts(date); + + const year = parts.find(part => part.type === 'year')?.value; + const month = parts.find(part => part.type === 'month')?.value; + const day = parts.find(part => part.type === 'day')?.value; + const hour = parts.find(part => part.type === 'hour')?.value; + const minute = parts.find(part => part.type === 'minute')?.value; + const second = parts.find(part => part.type === 'second')?.value; + + return format + .replace('dd', day || '') + .replace('MM', month || '') + .replace('yyyy', year || '') + .replace('HH', hour || '') + .replace('mm', minute || '') + .replace('ss', second || ''); + } + + /** + * Obtém timestamp atual no horário brasileiro + * @returns Timestamp em milissegundos + */ + static nowTimestamp(): number { + return Date.now(); + } + + /** + * Converte timestamp para data no horário brasileiro + * @param timestamp Timestamp em milissegundos + * @returns Data no horário brasileiro + */ + static fromTimestamp(timestamp: number): Date { + return new Date(timestamp); + } + + /** + * Verifica se uma data está no horário de verão brasileiro + * @param date Data a ser verificada + * @returns true se estiver no horário de verão + */ + static isBrazilianDaylightSavingTime(date: Date): boolean { + const year = date.getFullYear(); + + // Horário de verão no Brasil geralmente vai de outubro a fevereiro + // (regras podem variar, esta é uma implementação simplificada) + const october = new Date(year, 9, 1); // Outubro + const february = new Date(year + 1, 1, 1); // Fevereiro do ano seguinte + + return date >= october || date < february; + } + + /** + * Obtém o offset do timezone brasileiro em minutos + * @param date Data para calcular o offset + * @returns Offset em minutos (negativo para oeste) + */ + static getBrazilTimezoneOffset(date: Date): number { + const utc = new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); + const brazil = new Date(utc.toLocaleString('en-US', { timeZone: this.BRAZIL_TIMEZONE })); + return (utc.getTime() - brazil.getTime()) / 60000; + } +}