first
This commit is contained in:
233
package-lock.json
generated
233
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<SessionsResponseDto> {
|
||||
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<LoginAuditResponseDto> {
|
||||
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<LoginStatsDto> {
|
||||
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)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
139
src/auth/auth/dto/login-audit.dto.ts
Normal file
139
src/auth/auth/dto/login-audit.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
295
src/auth/services/login-audit.service.ts
Normal file
295
src/auth/services/login-audit.service.ts
Normal file
@@ -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<LoginAuditLog, 'id' | 'timestamp'>): Promise<void> {
|
||||
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<LoginAuditLog[]> {
|
||||
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<string, number>();
|
||||
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<string, { attempts: number; successes: number; failures: number }>();
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string>(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<RateLimitConfig>): Promise<void> {
|
||||
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<string>(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
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string> {
|
||||
async generateRefreshToken(userId: number, sessionId?: string): Promise<string> {
|
||||
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<RefreshTokenData>(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<RefreshTokenData>(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);
|
||||
|
||||
@@ -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<SessionData> {
|
||||
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<SessionData>(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<SessionData | null> {
|
||||
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
|
||||
|
||||
@@ -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<any>(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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
1
src/core/configs/cache/IRedisClient.ts
vendored
1
src/core/configs/cache/IRedisClient.ts
vendored
@@ -5,5 +5,6 @@ export interface IRedisClient {
|
||||
del(...keys: string[]): Promise<void>;
|
||||
keys(pattern: string): Promise<string[]>;
|
||||
ttl(key: string): Promise<number>;
|
||||
eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any>;
|
||||
}
|
||||
|
||||
@@ -35,4 +35,8 @@ export class RedisClientAdapter implements IRedisClient {
|
||||
async ttl(key: string): Promise<number> {
|
||||
return this.redis.ttl(key);
|
||||
}
|
||||
|
||||
async eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any> {
|
||||
return this.redis.eval(script, numKeys, ...keysAndArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
21
src/data-consult/clientes.controller.ts
Normal file
21
src/data-consult/clientes.controller.ts
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
19
src/data-consult/clientes.module.ts
Normal file
19
src/data-consult/clientes.module.ts
Normal file
@@ -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 { }
|
||||
154
src/data-consult/clientes.service.ts
Normal file
154
src/data-consult/clientes.service.ts
Normal file
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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<ProductDto[]> {
|
||||
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<CarrierDto[]> {
|
||||
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<CarrierDto[]> {
|
||||
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<CarrierDto[]> {
|
||||
return this.dataConsultService.getOrderCarriers(orderId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,7 @@ export class DataConsultRepository {
|
||||
return results.map(result => new StoreDto(result));
|
||||
}
|
||||
|
||||
async findSellers(): Promise<SellerDto[]> {
|
||||
async findSellers(): Promise<SellerDto[]> {
|
||||
const sql = `
|
||||
SELECT PCUSUARI.CODUSUR as "id",
|
||||
PCUSUARI.NOME as "name"
|
||||
@@ -55,51 +55,166 @@ export class DataConsultRepository {
|
||||
|
||||
async findBillings(): Promise<BillingDto[]> {
|
||||
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<BillingDto[]>(sql);
|
||||
return results.map(result => new BillingDto(result));
|
||||
}
|
||||
|
||||
async findCustomers(filter: string): Promise<CustomerDto[]> {
|
||||
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<CustomerDto[]>(sql, [`%${filter}%`]);
|
||||
return results.map(result => new CustomerDto(result));
|
||||
customers = await this.executeQuery<CustomerDto[]>(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<CustomerDto[]>(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<CustomerDto[]>(sql, [likeFilter]);
|
||||
}
|
||||
|
||||
return customers.map(row => new CustomerDto(row));
|
||||
}
|
||||
|
||||
async findProducts(filter: string): Promise<ProductDto[]> {
|
||||
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<ProductDto[]>(sql, [`%${filter}%`]);
|
||||
const results = await this.executeQuery<ProductDto[]>(sql, [filter]);
|
||||
return results.map(result => new ProductDto(result));
|
||||
}
|
||||
|
||||
async findAllProducts(): Promise<ProductDto[]> {
|
||||
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<ProductDto[]>(sql);
|
||||
return results.map(result => new ProductDto(result));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca todas as transportadoras cadastradas no sistema
|
||||
*/
|
||||
async findAllCarriers(): Promise<any[]> {
|
||||
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<any[]>(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca as transportadoras por período de data
|
||||
*/
|
||||
async findCarriersByDate(query: any): Promise<any[]> {
|
||||
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<any[]>(sql, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca as transportadoras de um pedido específico
|
||||
*/
|
||||
async findOrderCarriers(orderId: number): Promise<any[]> {
|
||||
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<any[]>(sql, [orderId]);
|
||||
}
|
||||
}
|
||||
@@ -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<CarrierDto[]> {
|
||||
this.logger.log('Buscando todas as transportadoras');
|
||||
try {
|
||||
return getOrSetCache<CarrierDto[]>(
|
||||
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<CarrierDto[]> {
|
||||
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<CarrierDto[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/data-consult/dto/carrier.dto.ts
Normal file
58
src/data-consult/dto/carrier.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<NestExpressApplication>(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'],
|
||||
|
||||
@@ -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<CutItemDto[]> {
|
||||
const itens = await this.ordersRepository.getCutItens(orderId);
|
||||
/**
|
||||
* Buscar entregas do pedido com cache
|
||||
*/
|
||||
async getOrderDeliveries(
|
||||
orderId: string,
|
||||
query: { createDateIni: string; createDateEnd: string },
|
||||
): Promise<OrderDeliveryDto[]> {
|
||||
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<CutItemDto[]> {
|
||||
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<OrderDeliveryDto> {
|
||||
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<LeadtimeDto[]> {
|
||||
const key = `orders:leadtime:${orderId}`;
|
||||
return getOrSetCache(
|
||||
this.redisClient,
|
||||
key,
|
||||
this.TTL_LEADTIME,
|
||||
() => this.ordersRepository.getLeadtimeWMS(orderId)
|
||||
);
|
||||
}
|
||||
|
||||
async getTransfer(orderId: number): Promise<OrderTransferDto[] | null> {
|
||||
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<EstLogTransferResponseDto[] | null> {
|
||||
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<EstLogTransferResponseDto[] | null> {
|
||||
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<OrderStatusDto[] | null> {
|
||||
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<CarrierDto[]> {
|
||||
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<MarkData> {
|
||||
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<MarkData[]> {
|
||||
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<MarkData[]> {
|
||||
const key = `orders:marks:name:${markName}`;
|
||||
|
||||
return getOrSetCache(this.redisClient, key, this.TTL_MARKS, async () => {
|
||||
return await this.ordersRepository.getMarksByName(markName);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<InvoiceDto> {
|
||||
@@ -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<OrderItemDto[]> {
|
||||
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> {
|
||||
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<CutItemDto[]> {
|
||||
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> {
|
||||
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<OrderDeliveryDto | null> {
|
||||
async getOrderDelivery(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderDeliveryDto | null> {
|
||||
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<OrderTransferDto[] | null> {
|
||||
async getTransfer(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderTransferDto[] | null> {
|
||||
try {
|
||||
return await this.ordersService.getTransfer(orderId);
|
||||
} catch (error) {
|
||||
@@ -113,8 +175,9 @@ async getTransfer(@Param('orderId') orderId: number): Promise<OrderTransferDto[]
|
||||
|
||||
@Get('status/:orderId')
|
||||
@ApiOperation({ summary: 'Consulta status do pedido' })
|
||||
@ApiParam({ name: 'orderId', example: 236001388 })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getStatusOrder(@Param('orderId') orderId: number): Promise<OrderStatusDto[] | null> {
|
||||
async getStatusOrder(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderStatusDto[] | null> {
|
||||
try {
|
||||
return await this.ordersService.getStatusOrder(orderId);
|
||||
} catch (error) {
|
||||
@@ -125,6 +188,43 @@ async getStatusOrder(@Param('orderId') orderId: number): Promise<OrderStatusDto[
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Get(':orderId/deliveries')
|
||||
@ApiOperation({ summary: 'Consulta entregas do pedido' })
|
||||
@ApiParam({ name: 'orderId', example: '236001388' })
|
||||
@ApiQuery({ name: 'createDateIni', required: false, description: 'Data inicial para filtro (formato YYYY-MM-DD)' })
|
||||
@ApiQuery({ name: 'createDateEnd', required: false, description: 'Data final para filtro (formato YYYY-MM-DD)' })
|
||||
async getOrderDeliveries(
|
||||
@Param('orderId', ParseIntPipe) orderId: number,
|
||||
@Query('createDateIni') createDateIni?: string,
|
||||
@Query('createDateEnd') createDateEnd?: string,
|
||||
): Promise<OrderDeliveryDto[]> {
|
||||
// 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<LeadtimeDto[]> {
|
||||
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<CarrierDto[]> {
|
||||
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<MarkResponseDto> {
|
||||
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<MarkResponseDto[]> {
|
||||
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<MarkResponseDto[]> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
src/orders/dto/emitente.dto.ts
Normal file
27
src/orders/dto/emitente.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
110
src/orders/dto/estlogtransfer.dto.ts
Normal file
110
src/orders/dto/estlogtransfer.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
113
src/orders/dto/find-orders-by-delivery-date.dto.ts
Normal file
113
src/orders/dto/find-orders-by-delivery-date.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
9
src/orders/dto/leadtime.dto.ts
Normal file
9
src/orders/dto/leadtime.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class LeadtimeDto {
|
||||
orderId: number;
|
||||
etapa: number;
|
||||
descricaoEtapa: string;
|
||||
data: Date | string;
|
||||
codigoFuncionario: number | null;
|
||||
nomeFuncionario: string | null;
|
||||
numeroPedido: number;
|
||||
}
|
||||
21
src/orders/dto/mark-response.dto.ts
Normal file
21
src/orders/dto/mark-response.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
23
src/orders/dto/order-delivery.dto.ts
Normal file
23
src/orders/dto/order-delivery.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
310
src/orders/dto/order-response.dto.ts
Normal file
310
src/orders/dto/order-response.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
20
src/orders/interceptors/orders-response.interceptor.ts
Normal file
20
src/orders/interceptors/orders-response.interceptor.ts
Normal file
@@ -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<T> implements NestInterceptor<T, ResultModel<T>> {
|
||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
return ResultModel.success(data);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/orders/interface/markdata.ts
Normal file
5
src/orders/interface/markdata.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface MarkData {
|
||||
MARCA: string;
|
||||
CODMARCA: number;
|
||||
ATIVO: string;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
16
src/partners/dto/partner.dto.ts
Normal file
16
src/partners/dto/partner.dto.ts
Normal file
@@ -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<PartnerDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
48
src/partners/partners.controller.ts
Normal file
48
src/partners/partners.controller.ts
Normal file
@@ -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<PartnerDto[]> {
|
||||
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<PartnerDto[]> {
|
||||
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<PartnerDto | null> {
|
||||
return this.partnersService.getPartnerById(id);
|
||||
}
|
||||
}
|
||||
13
src/partners/partners.module.ts
Normal file
13
src/partners/partners.module.ts
Normal file
@@ -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 {}
|
||||
168
src/partners/partners.service.ts
Normal file
168
src/partners/partners.service.ts
Normal file
@@ -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<PartnerDto[]> {
|
||||
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<PartnerDto[]> {
|
||||
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<PartnerDto | null> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
113
src/shared/date.util.ts
Normal file
113
src/shared/date.util.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user