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"
|
"@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": {
|
"node_modules/@inquirer/checkbox": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.6.tgz",
|
||||||
@@ -793,15 +803,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/core": {
|
"node_modules/@inquirer/core": {
|
||||||
"version": "10.1.11",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz",
|
||||||
"integrity": "sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==",
|
"integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/figures": "^1.0.11",
|
"@inquirer/ansi": "^1.0.0",
|
||||||
"@inquirer/type": "^3.0.6",
|
"@inquirer/figures": "^1.0.13",
|
||||||
"ansi-escapes": "^4.3.2",
|
"@inquirer/type": "^3.0.8",
|
||||||
"cli-width": "^4.1.0",
|
"cli-width": "^4.1.0",
|
||||||
"mute-stream": "^2.0.0",
|
"mute-stream": "^2.0.0",
|
||||||
"signal-exit": "^4.1.0",
|
"signal-exit": "^4.1.0",
|
||||||
@@ -821,15 +831,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/editor": {
|
"node_modules/@inquirer/editor": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.20",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz",
|
||||||
"integrity": "sha512-YoZr0lBnnLFPpfPSNsQ8IZyKxU47zPyVi9NLjCWtna52//M/xuL0PGPAxHxxYhdOhnvY2oBafoM+BI5w/JK7jw==",
|
"integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/core": "^10.1.11",
|
"@inquirer/core": "^10.2.2",
|
||||||
"@inquirer/type": "^3.0.6",
|
"@inquirer/external-editor": "^1.0.2",
|
||||||
"external-editor": "^3.1.0"
|
"@inquirer/type": "^3.0.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/@inquirer/figures": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
|
||||||
"integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==",
|
"integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1046,9 +1095,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/type": {
|
"node_modules/@inquirer/type": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz",
|
||||||
"integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
|
"integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3485,13 +3534,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.9.0",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3822,9 +3871,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4130,9 +4179,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chardet": {
|
"node_modules/chardet": {
|
||||||
"version": "0.7.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -5480,34 +5529,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/extglob": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
|
||||||
@@ -5776,14 +5797,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -6051,9 +6073,9 @@
|
|||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7613,15 +7635,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsdom/node_modules/form-data": {
|
"node_modules/jsdom/node_modules/form-data": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
|
||||||
"integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==",
|
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.35"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8687,16 +8710,6 @@
|
|||||||
"node": ">=14.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": {
|
"node_modules/p-each-series": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",
|
||||||
@@ -10312,16 +10325,23 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/sha.js": {
|
"node_modules/sha.js": {
|
||||||
"version": "2.4.11",
|
"version": "2.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||||
"license": "(MIT AND BSD-3-Clause)",
|
"license": "(MIT AND BSD-3-Clause)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.1",
|
"inherits": "^2.0.4",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.2.1",
|
||||||
|
"to-buffer": "^1.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sha.js": "bin.js"
|
"sha.js": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
@@ -11347,19 +11367,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
@@ -11367,6 +11374,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/to-object-path": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
|
||||||
@@ -11767,6 +11794,20 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/typedarray": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
@@ -11890,9 +11931,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typeorm/node_modules/brace-expansion": {
|
"node_modules/typeorm/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler';
|
|||||||
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
|
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
|
||||||
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
|
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { clientes } from './data-consult/clientes.module';
|
||||||
|
import { PartnersModule } from './partners/partners.module';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -57,6 +59,7 @@ import { HealthModule } from './health/health.module';
|
|||||||
OrdersPaymentModule,
|
OrdersPaymentModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
|
clientes,
|
||||||
ProductsModule,
|
ProductsModule,
|
||||||
NegotiationsModule,
|
NegotiationsModule,
|
||||||
OccurrencesModule,
|
OccurrencesModule,
|
||||||
@@ -66,6 +69,7 @@ import { HealthModule } from './health/health.module';
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
|
PartnersModule,
|
||||||
],
|
],
|
||||||
controllers: [OcorrencesController, LogisticController ],
|
controllers: [OcorrencesController, LogisticController ],
|
||||||
providers: [ LogisticService,],
|
providers: [ LogisticService,],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
Param,
|
Param,
|
||||||
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { CqrsModule } 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 { LoginResponseDto } from './dto/LoginResponseDto';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { ResultModel } from 'src/core/models/result.model';
|
import { ResultModel } from 'src/core/models/result.model';
|
||||||
|
import { DateUtil } from 'src/shared/date.util';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { RateLimitingGuard } from '../guards/rate-limiting.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 { SessionManagementService } from '../services/session-management.service';
|
||||||
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto';
|
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto';
|
||||||
import { SessionsResponseDto } from './dto/session.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 {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
@@ -44,6 +53,7 @@ export class AuthController {
|
|||||||
private readonly rateLimitingService: RateLimitingService,
|
private readonly rateLimitingService: RateLimitingService,
|
||||||
private readonly refreshTokenService: RefreshTokenService,
|
private readonly refreshTokenService: RefreshTokenService,
|
||||||
private readonly sessionManagementService: SessionManagementService,
|
private readonly sessionManagementService: SessionManagementService,
|
||||||
|
private readonly loginAuditService: LoginAuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@@ -62,30 +72,47 @@ export class AuthController {
|
|||||||
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
||||||
const result = await this.commandBus.execute(command);
|
const result = await this.commandBus.execute(command);
|
||||||
|
|
||||||
|
const userAgent = req.headers['user-agent'] || 'Unknown';
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
// Registra tentativa falhada
|
|
||||||
await this.rateLimitingService.recordAttempt(ip, false);
|
await this.rateLimitingService.recordAttempt(ip, false);
|
||||||
|
|
||||||
|
await this.loginAuditService.logLoginAttempt({
|
||||||
|
username: dto.username,
|
||||||
|
ipAddress: ip,
|
||||||
|
userAgent,
|
||||||
|
success: false,
|
||||||
|
failureReason: result.error,
|
||||||
|
});
|
||||||
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
new ResultModel(false, result.error, null, result.error),
|
new ResultModel(false, result.error, null, result.error),
|
||||||
HttpStatus.UNAUTHORIZED,
|
HttpStatus.UNAUTHORIZED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registra tentativa bem-sucedida (limpa contador)
|
|
||||||
await this.rateLimitingService.recordAttempt(ip, true);
|
await this.rateLimitingService.recordAttempt(ip, true);
|
||||||
|
|
||||||
const user = result.data;
|
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(
|
const session = await this.sessionManagementService.createSession(
|
||||||
user.id,
|
user.id,
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cria tokens de acesso e refresh com sessionId
|
|
||||||
const tokenPair = await this.authService.createTokenPair(
|
const tokenPair = await this.authService.createTokenPair(
|
||||||
user.id,
|
user.id,
|
||||||
user.sellerId,
|
user.sellerId,
|
||||||
@@ -95,11 +122,20 @@ export class AuthController {
|
|||||||
session.sessionId,
|
session.sessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.loginAuditService.logLoginAttempt({
|
||||||
|
userId: user.id,
|
||||||
|
username: dto.username,
|
||||||
|
ipAddress: ip,
|
||||||
|
userAgent,
|
||||||
|
success: true,
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
sellerId: user.sellerId,
|
sellerId: user.sellerId,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.name,
|
username: dto.username,
|
||||||
storeId: user.storeId,
|
storeId: user.storeId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
accessToken: tokenPair.accessToken,
|
accessToken: tokenPair.accessToken,
|
||||||
@@ -173,7 +209,7 @@ export class AuthController {
|
|||||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||||
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
||||||
const userId = req.user.id;
|
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);
|
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -181,8 +217,8 @@ export class AuthController {
|
|||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
ipAddress: session.ipAddress,
|
ipAddress: session.ipAddress,
|
||||||
userAgent: session.userAgent,
|
userAgent: session.userAgent,
|
||||||
createdAt: new Date(session.createdAt).toISOString(),
|
createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)),
|
||||||
lastActivity: new Date(session.lastActivity).toISOString(),
|
lastActivity: DateUtil.toBrazilISOString(new Date(session.lastActivity)),
|
||||||
isCurrent: session.sessionId === currentSessionId,
|
isCurrent: session.sessionId === currentSessionId,
|
||||||
})),
|
})),
|
||||||
total: sessions.length,
|
total: sessions.length,
|
||||||
@@ -222,4 +258,132 @@ export class AuthController {
|
|||||||
message: 'Todas as sessões foram encerradas com sucesso',
|
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 { RateLimitingService } from '../services/rate-limiting.service';
|
||||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||||
import { SessionManagementService } from '../services/session-management.service';
|
import { SessionManagementService } from '../services/session-management.service';
|
||||||
|
import { LoginAuditService } from '../services/login-audit.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,6 +41,7 @@ import { SessionManagementService } from '../services/session-management.service
|
|||||||
RateLimitingService,
|
RateLimitingService,
|
||||||
RefreshTokenService,
|
RefreshTokenService,
|
||||||
SessionManagementService,
|
SessionManagementService,
|
||||||
|
LoginAuditService,
|
||||||
AuthenticateUserHandler
|
AuthenticateUserHandler
|
||||||
],
|
],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ export class AuthService {
|
|||||||
* @param username Nome de usuário
|
* @param username Nome de usuário
|
||||||
* @param email Email do usuário
|
* @param email Email do usuário
|
||||||
* @param storeId ID da loja
|
* @param storeId ID da loja
|
||||||
|
* @param sessionId ID da sessão
|
||||||
* @returns Objeto com access token e refresh token
|
* @returns Objeto com access token e refresh token
|
||||||
*/
|
*/
|
||||||
async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
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 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 {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -68,7 +69,8 @@ export class AuthService {
|
|||||||
user.sellerId,
|
user.sellerId,
|
||||||
user.name,
|
user.name,
|
||||||
user.email,
|
user.email,
|
||||||
user.storeId
|
user.storeId,
|
||||||
|
tokenData.sessionId
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -85,7 +87,7 @@ export class AuthService {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
sellerId: user.sellerId,
|
sellerId: user.sellerId,
|
||||||
storeId: user.storeId,
|
storeId: user.storeId,
|
||||||
username: user.name, // Usando name como username para consistência
|
username: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -106,4 +108,13 @@ export class AuthService {
|
|||||||
async isTokenBlacklisted(token: string): Promise<boolean> {
|
async isTokenBlacklisted(token: string): Promise<boolean> {
|
||||||
return this.tokenBlacklistService.isBlacklisted(token);
|
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 ip Endereço IP do cliente
|
||||||
* @param config Configuração personalizada (opcional)
|
* @param config Configuração personalizada (opcional)
|
||||||
* @returns true se permitido, false se bloqueado
|
* @returns true se permitido, false se bloqueado
|
||||||
@@ -31,23 +31,51 @@ export class RateLimitingService {
|
|||||||
const key = this.buildAttemptKey(ip);
|
const key = this.buildAttemptKey(ip);
|
||||||
const blockKey = this.buildBlockKey(ip);
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|
||||||
// Verifica se está bloqueado
|
/**
|
||||||
const isBlocked = await this.redis.get(blockKey);
|
* Usa script Lua para operação atômica (verificação e incremento em uma única operação)
|
||||||
if (isBlocked) {
|
*/
|
||||||
return false;
|
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])
|
||||||
|
|
||||||
// Conta tentativas na janela de tempo
|
-- Verifica se já está bloqueado
|
||||||
const attempts = await this.redis.get<string>(key);
|
local isBlocked = redis.call('GET', blockKey)
|
||||||
const attemptCount = attempts ? parseInt(attempts) : 0;
|
if isBlocked then
|
||||||
|
return {0, 1} -- attempts=0, blocked=1
|
||||||
|
end
|
||||||
|
|
||||||
if (attemptCount >= finalConfig.maxAttempts) {
|
-- Incrementa contador de tentativas
|
||||||
// Bloqueia o IP
|
local attempts = redis.call('INCR', key)
|
||||||
await this.redis.set(blockKey, 'blocked', finalConfig.blockDurationMs / 1000);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
-- 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
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.redis.eval(
|
||||||
|
luaScript,
|
||||||
|
2,
|
||||||
|
key,
|
||||||
|
blockKey,
|
||||||
|
finalConfig.maxAttempts,
|
||||||
|
finalConfig.windowMs,
|
||||||
|
finalConfig.blockDurationMs
|
||||||
|
) as [number, number];
|
||||||
|
|
||||||
|
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> {
|
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> {
|
||||||
const finalConfig = { ...this.defaultConfig, ...config };
|
const finalConfig = { ...this.defaultConfig, ...config };
|
||||||
const key = this.buildAttemptKey(ip);
|
const key = this.buildAttemptKey(ip);
|
||||||
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
/**
|
||||||
|
* Limpa tentativas e bloqueio em caso de sucesso
|
||||||
|
*/
|
||||||
await this.redis.del(key);
|
await this.redis.del(key);
|
||||||
} else {
|
await this.redis.del(blockKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 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 { JwtService } from '@nestjs/jwt';
|
||||||
import { JwtPayload } from '../models/jwt-payload.model';
|
import { JwtPayload } from '../models/jwt-payload.model';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
|
import { DateUtil } from 'src/shared/date.util';
|
||||||
|
|
||||||
export interface RefreshTokenData {
|
export interface RefreshTokenData {
|
||||||
userId: number;
|
userId: number;
|
||||||
tokenId: string;
|
tokenId: string;
|
||||||
|
sessionId?: string;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
@@ -25,26 +27,30 @@ export class RefreshTokenService {
|
|||||||
/**
|
/**
|
||||||
* Gera um novo refresh token para o usuário
|
* Gera um novo refresh token para o usuário
|
||||||
* @param userId ID do usuário
|
* @param userId ID do usuário
|
||||||
|
* @param sessionId ID da sessão (opcional)
|
||||||
* @returns Refresh token
|
* @returns Refresh token
|
||||||
*/
|
*/
|
||||||
async generateRefreshToken(userId: number): Promise<string> {
|
async generateRefreshToken(userId: number, sessionId?: string): Promise<string> {
|
||||||
const tokenId = randomBytes(32).toString('hex');
|
const tokenId = randomBytes(32).toString('hex');
|
||||||
const refreshToken = this.jwtService.sign(
|
const refreshToken = this.jwtService.sign(
|
||||||
{ userId, tokenId, type: 'refresh' },
|
{ userId, tokenId, sessionId, type: 'refresh' },
|
||||||
{ expiresIn: '7d' }
|
{ expiresIn: '7d' }
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenData: RefreshTokenData = {
|
const tokenData: RefreshTokenData = {
|
||||||
userId,
|
userId,
|
||||||
tokenId,
|
tokenId,
|
||||||
expiresAt: Date.now() + (this.REFRESH_TOKEN_TTL * 1000),
|
sessionId,
|
||||||
createdAt: Date.now(),
|
expiresAt: DateUtil.nowTimestamp() + (this.REFRESH_TOKEN_TTL * 1000),
|
||||||
|
createdAt: DateUtil.nowTimestamp(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const key = this.buildRefreshTokenKey(userId, tokenId);
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||||
await this.redis.set(key, tokenData, this.REFRESH_TOKEN_TTL);
|
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);
|
await this.limitRefreshTokensPerUser(userId);
|
||||||
|
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
@@ -63,7 +69,7 @@ export class RefreshTokenService {
|
|||||||
throw new UnauthorizedException('Token inválido');
|
throw new UnauthorizedException('Token inválido');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId, tokenId } = decoded;
|
const { userId, tokenId, sessionId } = decoded;
|
||||||
const key = this.buildRefreshTokenKey(userId, tokenId);
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||||
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
||||||
|
|
||||||
@@ -71,7 +77,7 @@ export class RefreshTokenService {
|
|||||||
throw new UnauthorizedException('Refresh token expirado ou inválido');
|
throw new UnauthorizedException('Refresh token expirado ou inválido');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenData.expiresAt < Date.now()) {
|
if (tokenData.expiresAt < DateUtil.nowTimestamp()) {
|
||||||
await this.revokeRefreshToken(userId, tokenId);
|
await this.revokeRefreshToken(userId, tokenId);
|
||||||
throw new UnauthorizedException('Refresh token expirado');
|
throw new UnauthorizedException('Refresh token expirado');
|
||||||
}
|
}
|
||||||
@@ -82,6 +88,7 @@ export class RefreshTokenService {
|
|||||||
storeId: '',
|
storeId: '',
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
sessionId: sessionId || tokenData.sessionId,
|
||||||
tokenId
|
tokenId
|
||||||
} as JwtPayload;
|
} as JwtPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,7 +132,7 @@ export class RefreshTokenService {
|
|||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
||||||
if (tokenData && tokenData.expiresAt > Date.now()) {
|
if (tokenData && tokenData.expiresAt > DateUtil.nowTimestamp()) {
|
||||||
tokens.push(tokenData);
|
tokens.push(tokenData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +148,9 @@ export class RefreshTokenService {
|
|||||||
const activeTokens = await this.getActiveRefreshTokens(userId);
|
const activeTokens = await this.getActiveRefreshTokens(userId);
|
||||||
|
|
||||||
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
||||||
// Remove os tokens mais antigos
|
/**
|
||||||
|
* Remove os tokens mais antigos
|
||||||
|
*/
|
||||||
const tokensToRemove = activeTokens
|
const tokensToRemove = activeTokens
|
||||||
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
||||||
.map(token => token.tokenId);
|
.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 { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
|
import { DateUtil } from 'src/shared/date.util';
|
||||||
|
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -16,7 +17,7 @@ export interface SessionData {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionManagementService {
|
export class SessionManagementService {
|
||||||
private readonly SESSION_TTL = 8 * 60 * 60; // 8 horas em segundos
|
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(
|
constructor(
|
||||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||||
@@ -31,7 +32,7 @@ export class SessionManagementService {
|
|||||||
*/
|
*/
|
||||||
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
||||||
const sessionId = randomBytes(16).toString('hex');
|
const sessionId = randomBytes(16).toString('hex');
|
||||||
const now = Date.now();
|
const now = DateUtil.nowTimestamp();
|
||||||
|
|
||||||
const sessionData: SessionData = {
|
const sessionData: SessionData = {
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -62,7 +63,7 @@ export class SessionManagementService {
|
|||||||
const sessionData = await this.redis.get<SessionData>(key);
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
|
||||||
if (sessionData) {
|
if (sessionData) {
|
||||||
sessionData.lastActivity = Date.now();
|
sessionData.lastActivity = DateUtil.nowTimestamp();
|
||||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +159,16 @@ export class SessionManagementService {
|
|||||||
return sessionData ? sessionData.isActive : false;
|
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
|
* Limita o número de sessões por usuário
|
||||||
* @param userId ID do usuário
|
* @param userId ID do usuário
|
||||||
|
|||||||
@@ -31,17 +31,34 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
throw new UnauthorizedException('Token foi invalidado');
|
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);
|
const cachedUser = await this.redis.get<any>(sessionKey);
|
||||||
|
|
||||||
if (cachedUser) {
|
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 {
|
return {
|
||||||
id: cachedUser.id,
|
id: cachedUser.id,
|
||||||
sellerId: cachedUser.sellerId,
|
sellerId: cachedUser.sellerId,
|
||||||
storeId: cachedUser.storeId,
|
storeId: cachedUser.storeId,
|
||||||
username: cachedUser.name,
|
username: cachedUser.username, // ← Corrigido: usar username em vez de name
|
||||||
email: cachedUser.email,
|
email: cachedUser.email,
|
||||||
name: cachedUser.name,
|
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');
|
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 = {
|
const userData = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
sellerId: user.sellerId,
|
sellerId: user.sellerId,
|
||||||
storeId: user.storeId,
|
storeId: user.storeId,
|
||||||
username: user.name,
|
username: user.name, // ← Manter name como username para compatibilidade
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
sessionId: payload.sessionId, // Inclui sessionId do token
|
sessionId: payload.sessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.redis.set(sessionKey, userData, 60 * 60 * 8);
|
await this.redis.set(sessionKey, userData, 60 * 60 * 8);
|
||||||
@@ -65,7 +89,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
return userData;
|
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]);
|
const result = await this.dataSource.query(sql, [id]);
|
||||||
return result[0] || null;
|
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>;
|
del(...keys: string[]): Promise<void>;
|
||||||
keys(pattern: string): Promise<string[]>;
|
keys(pattern: string): Promise<string[]>;
|
||||||
ttl(key: string): Promise<number>;
|
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> {
|
async ttl(key: string): Promise<number> {
|
||||||
return this.redis.ttl(key);
|
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'),
|
username: config.get('ORACLE_USER'),
|
||||||
password: config.get('ORACLE_PASSWORD'),
|
password: config.get('ORACLE_PASSWORD'),
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: config.get('NODE_ENV') === 'development',
|
logging: false,
|
||||||
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
||||||
extra: {
|
extra: {
|
||||||
poolMin: validPoolMin,
|
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 { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe, ParseIntPipe } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||||
import { DataConsultService } from './data-consult.service';
|
import { DataConsultService } from './data-consult.service';
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'
|
||||||
import { ProductDto } from './dto/product.dto';
|
import { ProductDto } from './dto/product.dto';
|
||||||
@@ -7,6 +7,7 @@ import { StoreDto } from './dto/store.dto';
|
|||||||
import { SellerDto } from './dto/seller.dto';
|
import { SellerDto } from './dto/seller.dto';
|
||||||
import { BillingDto } from './dto/billing.dto';
|
import { BillingDto } from './dto/billing.dto';
|
||||||
import { CustomerDto } from './dto/customer.dto';
|
import { CustomerDto } from './dto/customer.dto';
|
||||||
|
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
|
||||||
|
|
||||||
@ApiTags('DataConsult')
|
@ApiTags('DataConsult')
|
||||||
@Controller('api/v1/data-consult')
|
@Controller('api/v1/data-consult')
|
||||||
@@ -71,4 +72,35 @@ export class DataConsultController {
|
|||||||
return this.dataConsultService.getAllProducts();
|
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 { LoggerModule } from 'src/Log/logger.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
||||||
|
import { clientes } from './clientes.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LoggerModule, ConfigModule, RedisModule],
|
imports: [LoggerModule, ConfigModule, RedisModule, clientes],
|
||||||
controllers: [DataConsultController],
|
controllers: [DataConsultController],
|
||||||
providers: [
|
providers: [
|
||||||
DataConsultService,
|
DataConsultService,
|
||||||
|
|||||||
@@ -55,51 +55,166 @@ export class DataConsultRepository {
|
|||||||
|
|
||||||
async findBillings(): Promise<BillingDto[]> {
|
async findBillings(): Promise<BillingDto[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT PCPEDC.NUMPED as "id",
|
SELECT p.CODCOB, p.COBRANCA FROM PCCOB p
|
||||||
PCPEDC.DATA as "date",
|
|
||||||
PCPEDC.VLTOTAL as "total"
|
|
||||||
FROM PCPEDC
|
|
||||||
WHERE PCPEDC.POSICAO = 'F'
|
|
||||||
`;
|
`;
|
||||||
const results = await this.executeQuery<BillingDto[]>(sql);
|
const results = await this.executeQuery<BillingDto[]>(sql);
|
||||||
return results.map(result => new BillingDto(result));
|
return results.map(result => new BillingDto(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCustomers(filter: string): Promise<CustomerDto[]> {
|
async findCustomers(filter: string): Promise<CustomerDto[]> {
|
||||||
const sql = `
|
// 1) limpa todos os não-dígitos para buscas exatas
|
||||||
SELECT PCCLIENT.CODCLI as "id",
|
const cleanedDigits = filter.replace(/\D/g, '');
|
||||||
PCCLIENT.CLIENTE as "name",
|
|
||||||
PCCLIENT.CGCENT as "document"
|
// 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
|
FROM PCCLIENT
|
||||||
WHERE PCCLIENT.CLIENTE LIKE :filter
|
WHERE PCCLIENT.CODCLI = :0
|
||||||
OR PCCLIENT.CGCENT LIKE :filter
|
ORDER BY PCCLIENT.CLIENTE
|
||||||
`;
|
`;
|
||||||
const results = await this.executeQuery<CustomerDto[]>(sql, [`%${filter}%`]);
|
customers = await this.executeQuery<CustomerDto[]>(sql, [cleanedDigits]);
|
||||||
return results.map(result => new CustomerDto(result));
|
|
||||||
|
// --- 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[]> {
|
async findProducts(filter: string): Promise<ProductDto[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT PCPRODUT.CODPROD as "id",
|
SELECT PCPRODUT.CODPROD as "id",
|
||||||
PCPRODUT.DESCRICAO as "name",
|
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
|
||||||
PCPRODUT.CODFAB as "manufacturerCode"
|
|
||||||
FROM PCPRODUT
|
FROM PCPRODUT
|
||||||
WHERE PCPRODUT.DESCRICAO LIKE :filter
|
WHERE PCPRODUT.CODPROD = :filter
|
||||||
OR PCPRODUT.CODFAB LIKE :filter
|
|
||||||
`;
|
`;
|
||||||
const results = await this.executeQuery<ProductDto[]>(sql, [`%${filter}%`]);
|
const results = await this.executeQuery<ProductDto[]>(sql, [filter]);
|
||||||
return results.map(result => new ProductDto(result));
|
return results.map(result => new ProductDto(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllProducts(): Promise<ProductDto[]> {
|
async findAllProducts(): Promise<ProductDto[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT PCPRODUT.CODPROD as "id",
|
SELECT PCPRODUT.CODPROD as "id",
|
||||||
PCPRODUT.DESCRICAO as "name",
|
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
|
||||||
PCPRODUT.CODFAB as "manufacturerCode"
|
|
||||||
FROM PCPRODUT
|
FROM PCPRODUT
|
||||||
WHERE ROWNUM <= 500
|
WHERE ROWNUM <= 500
|
||||||
`;
|
`;
|
||||||
const results = await this.executeQuery<ProductDto[]>(sql);
|
const results = await this.executeQuery<ProductDto[]>(sql);
|
||||||
return results.map(result => new ProductDto(result));
|
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 { BillingDto } from './dto/billing.dto';
|
||||||
import { CustomerDto } from './dto/customer.dto';
|
import { CustomerDto } from './dto/customer.dto';
|
||||||
import { ProductDto } from './dto/product.dto';
|
import { ProductDto } from './dto/product.dto';
|
||||||
|
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
|
||||||
import { ILogger } from '../Log/ILogger';
|
import { ILogger } from '../Log/ILogger';
|
||||||
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
|
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
|
||||||
import { IRedisClient } from '../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../core/configs/cache/IRedisClient';
|
||||||
@@ -20,6 +21,9 @@ export class DataConsultService {
|
|||||||
private readonly BILLINGS_TTL = 3600;
|
private readonly BILLINGS_TTL = 3600;
|
||||||
private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all';
|
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(
|
constructor(
|
||||||
private readonly repository: DataConsultRepository,
|
private readonly repository: DataConsultRepository,
|
||||||
@@ -134,4 +138,71 @@ export class DataConsultService {
|
|||||||
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
|
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';
|
import { join } from 'path';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
/**
|
||||||
|
* Configura timezone para horário brasileiro
|
||||||
|
*/
|
||||||
|
process.env.TZ = 'America/Sao_Paulo';
|
||||||
|
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
@@ -46,8 +51,8 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: process.env.NODE_ENV === 'production'
|
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'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
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 { FindOrdersDto } from '../dto/find-orders.dto';
|
||||||
import { InvoiceDto } from '../dto/find-invoice.dto';
|
import { InvoiceDto } from '../dto/find-invoice.dto';
|
||||||
import { CutItemDto } from '../dto/CutItemDto';
|
import { CutItemDto } from '../dto/CutItemDto';
|
||||||
@@ -12,14 +12,24 @@ import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
|||||||
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
||||||
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
||||||
import { InvoiceCheckDto } from '../dto/invoice-check.dto';
|
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()
|
@Injectable()
|
||||||
export class OrdersService {
|
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_INVOICE = 60 * 60; // 1 hora
|
||||||
private readonly TTL_ITENS = 60 * 10; // 10 minutos
|
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(
|
constructor(
|
||||||
private readonly ordersRepository: OrdersRepository,
|
private readonly ordersRepository: OrdersRepository,
|
||||||
@@ -31,7 +41,6 @@ export class OrdersService {
|
|||||||
*/
|
*/
|
||||||
async findOrders(query: FindOrdersDto) {
|
async findOrders(query: FindOrdersDto) {
|
||||||
const key = `orders:query:${this.hashObject(query)}`;
|
const key = `orders:query:${this.hashObject(query)}`;
|
||||||
|
|
||||||
return getOrSetCache(
|
return getOrSetCache(
|
||||||
this.redisClient,
|
this.redisClient,
|
||||||
key,
|
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
|
* Buscar nota fiscal por chave NFe com cache
|
||||||
*/
|
*/
|
||||||
@@ -93,7 +161,27 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 getOrSetCache(
|
||||||
|
this.redisClient,
|
||||||
|
key,
|
||||||
|
this.TTL_DELIVERIES,
|
||||||
|
() => this.ordersRepository.getOrderDeliveries(orderId, query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getCutItens(orderId: string): Promise<CutItemDto[]> {
|
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);
|
const itens = await this.ordersRepository.getCutItens(orderId);
|
||||||
|
|
||||||
return itens.map(item => ({
|
return itens.map(item => ({
|
||||||
@@ -105,19 +193,73 @@ export class OrdersService {
|
|||||||
cutQuantity: Number(item.cutQuantity),
|
cutQuantity: Number(item.cutQuantity),
|
||||||
separedQuantity: Number(item.separedQuantity),
|
separedQuantity: Number(item.separedQuantity),
|
||||||
}));
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderDelivery(orderId: string): Promise<OrderDeliveryDto> {
|
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> {
|
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> {
|
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
|
||||||
*/
|
*/
|
||||||
@@ -126,9 +268,62 @@ export class OrdersService {
|
|||||||
return createHash('md5').update(str).digest('hex');
|
return createHash('md5').update(str).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
||||||
|
// Não usa cache para operações de escrita
|
||||||
return this.ordersRepository.createInvoiceCheck(invoice);
|
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,
|
ValidationPipe,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
DefaultValuePipe,
|
||||||
|
ParseBoolPipe,
|
||||||
} from '@nestjs/common';
|
} 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 { ResponseInterceptor } from '../../common/response.interceptor';
|
||||||
import { OrdersService } from '../application/orders.service';
|
import { OrdersService } from '../application/orders.service';
|
||||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
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 { InvoiceDto } from '../dto/find-invoice.dto';
|
||||||
import { OrderItemDto } from "../dto/OrderItemDto";
|
import { OrderItemDto } from "../dto/OrderItemDto";
|
||||||
|
import { LeadtimeDto } from '../dto/leadtime.dto';
|
||||||
import { CutItemDto } from '../dto/CutItemDto';
|
import { CutItemDto } from '../dto/CutItemDto';
|
||||||
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
||||||
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
||||||
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
||||||
import { InvoiceCheckDto } from '../dto/invoice-check.dto';
|
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')
|
@ApiTags('Orders')
|
||||||
@ApiBearerAuth()
|
//@ApiBearerAuth()
|
||||||
@UseGuards(JwtAuthGuard)
|
//@UseGuards(JwtAuthGuard)
|
||||||
@UseInterceptors(ResponseInterceptor)
|
|
||||||
@Controller('api/v1/orders')
|
@Controller('api/v1/orders')
|
||||||
export class OrdersController {
|
export class OrdersController {
|
||||||
constructor(private readonly ordersService: OrdersService) {}
|
constructor(private readonly ordersService: OrdersService) {}
|
||||||
|
|
||||||
@Get('find')
|
@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 }))
|
@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);
|
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')
|
@Get('invoice/:chavenfe')
|
||||||
|
@ApiParam({
|
||||||
|
name: 'chavenfe',
|
||||||
|
required: true,
|
||||||
|
description: 'Chave da Nota Fiscal (44 dígitos)',
|
||||||
|
example: '35191234567890000123550010000000011000000010',
|
||||||
|
})
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Busca NF pela chave' })
|
@ApiOperation({ summary: 'Busca NF pela chave' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
|
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
|
||||||
@@ -56,12 +113,14 @@ export class OrdersController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('itens/:orderId')
|
@Get('itens/:orderId')
|
||||||
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
|
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: '236001388' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getItens(@Param('orderId') orderId: string): Promise<OrderItemDto[]> {
|
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> {
|
||||||
try {
|
try {
|
||||||
return await this.ordersService.getItens(orderId);
|
return await this.ordersService.getItens(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
error.message || 'Erro ao buscar itens do pedido',
|
error.message || 'Erro ao buscar itens do pedido',
|
||||||
@@ -71,10 +130,11 @@ export class OrdersController {
|
|||||||
}
|
}
|
||||||
@Get('cut-itens/:orderId')
|
@Get('cut-itens/:orderId')
|
||||||
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
|
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: '236001388' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getCutItens(@Param('orderId') orderId: string): Promise<CutItemDto[]> {
|
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> {
|
||||||
try {
|
try {
|
||||||
return await this.ordersService.getCutItens(orderId);
|
return await this.ordersService.getCutItens(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
error.message || 'Erro ao buscar itens cortados',
|
error.message || 'Erro ao buscar itens cortados',
|
||||||
@@ -85,10 +145,11 @@ export class OrdersController {
|
|||||||
|
|
||||||
@Get('delivery/:orderId')
|
@Get('delivery/:orderId')
|
||||||
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
|
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: '236001388' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@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 {
|
try {
|
||||||
return await this.ordersService.getOrderDelivery(orderId);
|
return await this.ordersService.getOrderDelivery(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
error.message || 'Erro ao buscar dados de entrega',
|
error.message || 'Erro ao buscar dados de entrega',
|
||||||
@@ -99,8 +160,9 @@ export class OrdersController {
|
|||||||
|
|
||||||
@Get('transfer/:orderId')
|
@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 }))
|
@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 {
|
try {
|
||||||
return await this.ordersService.getTransfer(orderId);
|
return await this.ordersService.getTransfer(orderId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -113,8 +175,9 @@ async getTransfer(@Param('orderId') orderId: number): Promise<OrderTransferDto[]
|
|||||||
|
|
||||||
@Get('status/:orderId')
|
@Get('status/:orderId')
|
||||||
@ApiOperation({ summary: 'Consulta status do pedido' })
|
@ApiOperation({ summary: 'Consulta status do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: 236001388 })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@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 {
|
try {
|
||||||
return await this.ordersService.getStatusOrder(orderId);
|
return await this.ordersService.getStatusOrder(orderId);
|
||||||
} catch (error) {
|
} 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')
|
@Post('invoice/check')
|
||||||
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
|
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
@@ -138,4 +238,146 @@ async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: s
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ValidateNested } from 'class-validator';
|
||||||
|
import { CustomerDto } from 'src/data-consult/dto/customer.dto';
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@@ -6,6 +10,7 @@ import {
|
|||||||
IsNumber,
|
IsNumber,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
IsIn,
|
IsIn,
|
||||||
|
IsBoolean
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class FindOrdersDto {
|
export class FindOrdersDto {
|
||||||
@@ -14,22 +19,68 @@ export class FindOrdersDto {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
codfilial?: string;
|
codfilial?: string;
|
||||||
|
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Boolean)
|
||||||
|
@IsBoolean()
|
||||||
|
includeCheckout?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
|
|
||||||
filialretira?: string;
|
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;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
partnerId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
customerName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
stockId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
|
|
||||||
sellerId?: number;
|
sellerId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
sellerName?: string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
|
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -72,6 +123,11 @@ export class FindOrdersDto {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
invoiceDateEnd?: string;
|
invoiceDateEnd?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
deliveryDate?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
@@ -93,7 +149,73 @@ export class FindOrdersDto {
|
|||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(['S', 'N'])
|
@Type(() => Boolean)
|
||||||
|
@IsBoolean()
|
||||||
@ApiPropertyOptional()
|
@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