Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-02-21 16:03:37 +07:00
commit e310f49d41
28 changed files with 10675 additions and 118 deletions

View file

@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

8774
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -21,10 +21,12 @@
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"prisma": "^6.2.1",
"prisma": "^6.3.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
@ -32,19 +34,21 @@
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
"@prisma/client": "^6.2.1",
"@scalar/express-api-reference": "^0.4.173",
"@prisma/client": "^6.3.0",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/morgan": "^1.9.9",
"cors": "^2.8.5",
"cron": "^3.3.1",
"dayjs": "^1.11.13",
"dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"fast-jwt": "^4.0.6",
"fast-jwt": "^5.0.5",
"kysely": "^0.27.5",
"minio": "^8.0.2",
"morgan": "^1.10.0",
"nodemailer": "^6.10.0",
"prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6",
"thai-baht-text": "^2.0.5",

159
pnpm-lock.yaml generated
View file

@ -15,23 +15,26 @@ importers:
specifier: ^5.0.2
version: 5.0.2
'@prisma/client':
specifier: ^6.2.1
version: 6.2.1(prisma@6.2.1)
specifier: ^6.3.0
version: 6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)
'@scalar/express-api-reference':
specifier: ^0.4.173
version: 0.4.173
specifier: ^0.4.182
version: 0.4.182
'@tsoa/runtime':
specifier: ^6.6.0
version: 6.6.0
'@types/morgan':
specifier: ^1.9.9
version: 1.9.9
cors:
specifier: ^2.8.5
version: 2.8.5
cron:
specifier: ^3.3.1
version: 3.3.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
dayjs-plugin-utc:
specifier: ^0.1.2
version: 0.1.2
docx-templates:
specifier: ^4.13.0
version: 4.13.0
@ -42,8 +45,8 @@ importers:
specifier: ^4.21.2
version: 4.21.2
fast-jwt:
specifier: ^4.0.6
version: 4.0.6
specifier: ^5.0.5
version: 5.0.5
kysely:
specifier: ^0.27.5
version: 0.27.5
@ -53,9 +56,12 @@ importers:
morgan:
specifier: ^1.10.0
version: 1.10.0
nodemailer:
specifier: ^6.10.0
version: 6.10.0
prisma-extension-kysely:
specifier: ^3.0.0
version: 3.0.0(@prisma/client@6.2.1(prisma@6.2.1))
version: 3.0.0(@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2))
promise.any:
specifier: ^2.0.6
version: 2.0.6
@ -81,9 +87,15 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/morgan':
specifier: ^1.9.9
version: 1.9.9
'@types/node':
specifier: ^20.17.10
version: 20.17.10
'@types/nodemailer':
specifier: ^6.4.17
version: 6.4.17
nodemon:
specifier: ^3.1.9
version: 3.1.9
@ -91,8 +103,8 @@ importers:
specifier: ^3.4.2
version: 3.4.2
prisma:
specifier: ^6.2.1
version: 6.2.1
specifier: ^6.3.0
version: 6.3.0(typescript@5.7.2)
prisma-kysely:
specifier: ^1.8.0
version: 1.8.0(encoding@0.1.13)
@ -318,35 +330,38 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@prisma/client@6.2.1':
resolution: {integrity: sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==}
'@prisma/client@6.3.0':
resolution: {integrity: sha512-BY3Fi28PUSk447Bpv22LhZp4HgNPo7NsEN+EteM1CLDnLjig5863jpW+3c3HHLFmml+nB/eJv1CjSriFZ8z7Cg==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
typescript: '>=5.1.0'
peerDependenciesMeta:
prisma:
optional: true
typescript:
optional: true
'@prisma/debug@5.3.1':
resolution: {integrity: sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==}
'@prisma/debug@6.2.1':
resolution: {integrity: sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==}
'@prisma/debug@6.3.0':
resolution: {integrity: sha512-m1lQv//0Rc5RG8TBpNUuLCxC35Ghi5XfpPmL83Gh04/GICHD2J5H2ndMlaljrUNaQDF9dOxIuFAYP1rE9wkXkg==}
'@prisma/engines-version@6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69':
resolution: {integrity: sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==}
'@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0':
resolution: {integrity: sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==}
'@prisma/engines@5.3.1':
resolution: {integrity: sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==}
'@prisma/engines@6.2.1':
resolution: {integrity: sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==}
'@prisma/engines@6.3.0':
resolution: {integrity: sha512-RXqYhlZb9sx/xkUfYIZuEPn7sT0WgTxNOuEYQ7AGw3IMpP9QGVEDVsluc/GcNkM8NTJszeqk8AplJzI9lm7Jxw==}
'@prisma/fetch-engine@5.3.1':
resolution: {integrity: sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==}
'@prisma/fetch-engine@6.2.1':
resolution: {integrity: sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==}
'@prisma/fetch-engine@6.3.0':
resolution: {integrity: sha512-GBy0iT4f1mH31ePzfcpVSUa7JLRTeq4914FG2vR3LqDwRweSm4ja1o5flGDz+eVIa/BNYfkBvRRxv4D6ve6Eew==}
'@prisma/generator-helper@5.3.1':
resolution: {integrity: sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==}
@ -354,8 +369,8 @@ packages:
'@prisma/get-platform@5.3.1':
resolution: {integrity: sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==}
'@prisma/get-platform@6.2.1':
resolution: {integrity: sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==}
'@prisma/get-platform@6.3.0':
resolution: {integrity: sha512-V8zZ1d0xfyi6FjpNP4AcYuwSpGcdmu35OXWnTPm8IW594PYALzKXHwIa9+o0f+Lo9AecFWrwrwaoYe56UNfTtQ==}
'@prisma/internals@5.3.1':
resolution: {integrity: sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==}
@ -363,16 +378,16 @@ packages:
'@prisma/prisma-schema-wasm@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59':
resolution: {integrity: sha512-+zUI7NQDXfcNnU8HgrAj4jRMv8yRfITLzcfv0Urf0adKimM+hkkVG4rX38i9zWMlxekkEBw7NLFx3Gxxy8d3iQ==}
'@scalar/express-api-reference@0.4.173':
resolution: {integrity: sha512-C/s+rdImUb7gLbYRH6ICZjVPV8SHHMwJQ/Fiu6fVGomPldi0fpTDMaXH+9jRIlTm3LR6ACl/OlH7MXcPPRaFSA==}
'@scalar/express-api-reference@0.4.182':
resolution: {integrity: sha512-T8y+/FM24H1C13GMDkjmM62obEhfGclIPwR2tn2JK+TY6LhKuHAF0xPzaBkfiRjlSEnrJigJSJxb8lwGxHWr9A==}
engines: {node: '>=18'}
'@scalar/openapi-types@0.1.5':
resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==}
'@scalar/openapi-types@0.1.7':
resolution: {integrity: sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A==}
engines: {node: '>=18'}
'@scalar/types@0.0.25':
resolution: {integrity: sha512-sGnOFnfiSn4o23rklU/jrg81hO+630bsFIdHg8MZ/w2Nc6IoUwARA2hbe4d4Fg+D0KBu40Tan/L+WAYDXkTJQg==}
'@scalar/types@0.0.33':
resolution: {integrity: sha512-4mQYkQJO0HHaoFd8Z+vSdQAvYcCJ2bRLN9ewE+GneB8kvoLG/oM3ynroqzGQdoytH8BmhnJwD3aEUagfbK2x5g==}
engines: {node: '>=18'}
'@swc/helpers@0.5.15':
@ -468,6 +483,9 @@ packages:
'@types/node@20.17.10':
resolution: {integrity: sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==}
'@types/nodemailer@6.4.17':
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@ -850,6 +868,9 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
dayjs-plugin-utc@0.1.2:
resolution: {integrity: sha512-ExERH5o3oo6jFOdkvMP3gytTCQ9Ksi5PtylclJWghr7k7m3o2U5QrwtdiJkOxLOH4ghr0EKhpqGefzGz1VvVJg==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@ -1041,10 +1062,9 @@ packages:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
fast-jwt@4.0.6:
resolution: {integrity: sha512-kyNxaoy/m8pDQdyn+gl4FOTNEOrLK/1iAU4MP8cTz3mErOyh+BtvoCNFxKsyY4zlEKWO5HMY526zRXdoiPc6Rg==}
fast-jwt@5.0.5:
resolution: {integrity: sha512-Ch94zewwBjRznO0r76NFI5FDT0lOtnzkWVO4r7+d7E2WKuf7WW1FVOWRpv7QGEFlXzz9OAayrb5BhEmkOkwjhg==}
engines: {node: '>=20'}
deprecated: this package version has been deprecated
fast-redact@3.5.0:
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
@ -1784,6 +1804,10 @@ packages:
encoding:
optional: true
nodemailer@6.10.0:
resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==}
engines: {node: '>=6.0.0'}
nodemon@3.1.9:
resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==}
engines: {node: '>=10'}
@ -1986,10 +2010,15 @@ packages:
resolution: {integrity: sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==}
hasBin: true
prisma@6.2.1:
resolution: {integrity: sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==}
prisma@6.3.0:
resolution: {integrity: sha512-y+Zh3Qg+xGCWyyrNUUNaFW/OltaV/yXYuTa0WRgYkz5LGyifmAsgpv94I47+qGRocZrMGcbF2A/78/oO2zgifA==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
typescript: '>=5.1.0'
peerDependenciesMeta:
typescript:
optional: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@ -2983,9 +3012,10 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@prisma/client@6.2.1(prisma@6.2.1)':
'@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)':
optionalDependencies:
prisma: 6.2.1
prisma: 6.3.0(typescript@5.7.2)
typescript: 5.7.2
'@prisma/debug@5.3.1':
dependencies:
@ -2995,18 +3025,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@prisma/debug@6.2.1': {}
'@prisma/debug@6.3.0': {}
'@prisma/engines-version@6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69': {}
'@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {}
'@prisma/engines@5.3.1': {}
'@prisma/engines@6.2.1':
'@prisma/engines@6.3.0':
dependencies:
'@prisma/debug': 6.2.1
'@prisma/engines-version': 6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69
'@prisma/fetch-engine': 6.2.1
'@prisma/get-platform': 6.2.1
'@prisma/debug': 6.3.0
'@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0
'@prisma/fetch-engine': 6.3.0
'@prisma/get-platform': 6.3.0
'@prisma/fetch-engine@5.3.1(encoding@0.1.13)':
dependencies:
@ -3031,11 +3061,11 @@ snapshots:
- encoding
- supports-color
'@prisma/fetch-engine@6.2.1':
'@prisma/fetch-engine@6.3.0':
dependencies:
'@prisma/debug': 6.2.1
'@prisma/engines-version': 6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69
'@prisma/get-platform': 6.2.1
'@prisma/debug': 6.3.0
'@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0
'@prisma/get-platform': 6.3.0
'@prisma/generator-helper@5.3.1':
dependencies:
@ -3061,9 +3091,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@prisma/get-platform@6.2.1':
'@prisma/get-platform@6.3.0':
dependencies:
'@prisma/debug': 6.2.1
'@prisma/debug': 6.3.0
'@prisma/internals@5.3.1(encoding@0.1.13)':
dependencies:
@ -3115,15 +3145,15 @@ snapshots:
'@prisma/prisma-schema-wasm@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59': {}
'@scalar/express-api-reference@0.4.173':
'@scalar/express-api-reference@0.4.182':
dependencies:
'@scalar/types': 0.0.25
'@scalar/types': 0.0.33
'@scalar/openapi-types@0.1.5': {}
'@scalar/openapi-types@0.1.7': {}
'@scalar/types@0.0.25':
'@scalar/types@0.0.33':
dependencies:
'@scalar/openapi-types': 0.1.5
'@scalar/openapi-types': 0.1.7
'@unhead/schema': 1.11.14
'@swc/helpers@0.5.15':
@ -3258,6 +3288,10 @@ snapshots:
dependencies:
undici-types: 6.19.8
'@types/nodemailer@6.4.17':
dependencies:
'@types/node': 20.17.10
'@types/normalize-package-data@2.4.4': {}
'@types/qs@6.9.17': {}
@ -3724,6 +3758,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
dayjs-plugin-utc@0.1.2: {}
dayjs@1.11.13: {}
debug@2.6.9:
@ -4035,7 +4071,7 @@ snapshots:
merge2: 1.4.1
micromatch: 4.0.8
fast-jwt@4.0.6:
fast-jwt@5.0.5:
dependencies:
'@lukeed/ms': 2.0.2
asn1.js: 5.4.1
@ -4776,6 +4812,8 @@ snapshots:
optionalDependencies:
encoding: 0.1.13
nodemailer@6.10.0: {}
nodemon@3.1.9:
dependencies:
chokidar: 3.6.0
@ -4969,9 +5007,9 @@ snapshots:
prettier@3.4.2: {}
prisma-extension-kysely@3.0.0(@prisma/client@6.2.1(prisma@6.2.1)):
prisma-extension-kysely@3.0.0(@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)):
dependencies:
'@prisma/client': 6.2.1(prisma@6.2.1)
'@prisma/client': 6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)
prisma-kysely@1.8.0(encoding@0.1.13):
dependencies:
@ -4984,11 +5022,12 @@ snapshots:
- encoding
- supports-color
prisma@6.2.1:
prisma@6.3.0(typescript@5.7.2):
dependencies:
'@prisma/engines': 6.2.1
'@prisma/engines': 6.3.0
optionalDependencies:
fsevents: 2.3.3
typescript: 5.7.2
process-nextick-args@2.0.1: {}

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Institution" ADD COLUMN "status" "Status" NOT NULL DEFAULT 'CREATED',
ADD COLUMN "statusOrder" INTEGER NOT NULL DEFAULT 0;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "CustomerBranch" ADD COLUMN "userId" TEXT;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "CustomerBranch" ADD COLUMN "otpCode" TEXT,
ADD COLUMN "otpExpires" TEXT;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- The `otpExpires` column on the `CustomerBranch` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "CustomerBranch" DROP COLUMN "otpExpires",
ADD COLUMN "otpExpires" DATE;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- The `otpExpires` column on the `CustomerBranch` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "CustomerBranch" DROP COLUMN "otpExpires",
ADD COLUMN "otpExpires" TIME;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- The `otpExpires` column on the `CustomerBranch` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "CustomerBranch" DROP COLUMN "otpExpires",
ADD COLUMN "otpExpires" TIMESTAMP(3);

View file

@ -534,6 +534,10 @@ model CustomerBranch {
telephoneNo String
userId String?
otpCode String?
otpExpires DateTime?
// NOTE: About (Natural Person)
namePrefix String?
firstName String?
@ -986,6 +990,9 @@ model Institution {
subDistrict SubDistrict @relation(fields: [subDistrictId], references: [id], onDelete: Cascade)
subDistrictId String
status Status @default(CREATED)
statusOrder Int @default(0)
selectedImage String?
taskOrder TaskOrder[]

View file

@ -6,7 +6,6 @@ import morgan from "./middlewares/morgan";
import { RegisterRoutes } from "./routes";
import { initEmploymentOffice, initThailandAreaDatabase } from "./utils/thailand-area";
import { initFirstAdmin } from "./utils/database";
import { apiReference } from "@scalar/express-api-reference";
import { initSchedule } from "./services/schedule";
const APP_HOST = process.env.APP_HOST || "0.0.0.0";
@ -24,8 +23,7 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
const originalSend = app.response.json;
app.response.json = function (body: unknown) {
this.app.locals.response = body;
return originalSend.call(this, body);
return originalSend.call(this, (this.app.locals.response = body));
};
app.use(cors());
@ -36,11 +34,18 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
app.use("/", express.static("static"));
app.use(
"/api-docs",
apiReference({
theme: "kepler",
spec: { url: "/api/openapi" },
cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.74",
}),
await import("@scalar/express-api-reference").then(({ apiReference }) =>
apiReference({
theme: "kepler",
layout: "classic",
hideModels: true,
hideClientButton: true,
customCss: `.endpoint-label-name { display: none }`,
spec: { url: "/api/openapi" },
}),
),
);
RegisterRoutes(app);

View file

@ -645,14 +645,14 @@ export class CustomerBranchFileController extends Controller {
@Tags("Customer Branch Citizen")
async listCitizen(@Request() req: RequestWithUser, @Path() branchId: string) {
await this.checkPermission(req.user, branchId);
return await listFile(fileLocation.customerBranch.attachment(branchId));
return await listFile(fileLocation.customerBranch.citizen(branchId));
}
@Get("file-citizen/{id}")
@Security("keycloak")
@Tags("Customer Branch Citizen")
async getCitizen(@Path() branchId: string, @Path() id: string) {
return await getFile(fileLocation.customerBranch.attachment(branchId, id));
return await getFile(fileLocation.customerBranch.citizen(branchId, id));
}
@Put("file-citizen/{id}")
@ -660,7 +660,7 @@ export class CustomerBranchFileController extends Controller {
@Tags("Customer Branch Citizen")
async putCitizen(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) {
await this.checkPermission(req.user, branchId);
return req.res?.redirect(await setFile(fileLocation.customerBranch.attachment(branchId, id)));
return req.res?.redirect(await setFile(fileLocation.customerBranch.citizen(branchId, id)));
}
@Delete("file-citizen/{id}")

View file

@ -1,4 +1,4 @@
import { Prisma } from "@prisma/client";
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
@ -16,12 +16,13 @@ import {
Tags,
} from "tsoa";
import prisma from "../db";
import { notFoundError } from "../utils/error";
import { isUsedError, notFoundError } from "../utils/error";
import { queryOrNot } from "../utils/relation";
import { RequestWithUser } from "../interfaces/user";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { filterStatus } from "../services/prisma";
type InstitutionPayload = {
name: string;
@ -55,9 +56,11 @@ export class InstitutionController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
) {
return this.getInstitutionListByCriteria(query, page, pageSize, group);
return this.getInstitutionListByCriteria(query, page, pageSize, status, activeOnly, group);
}
@Post("list")
@ -67,6 +70,8 @@ export class InstitutionController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
@Body()
body?: {
@ -74,6 +79,7 @@ export class InstitutionController extends Controller {
},
) {
const where = {
...filterStatus(activeOnly ? Status.ACTIVE : status),
group: body?.group ? { in: body.group } : group,
OR: queryOrNot<Prisma.InstitutionWhereInput[]>(query, [
{ name: { contains: query } },
@ -89,6 +95,7 @@ export class InstitutionController extends Controller {
district: true,
subDistrict: true,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
take: pageSize,
skip: (page - 1) * pageSize,
}),
@ -115,7 +122,12 @@ export class InstitutionController extends Controller {
@Post()
@Security("keycloak")
@OperationId("createInstitution")
async createInstitution(@Body() body: InstitutionPayload) {
async createInstitution(
@Body()
body: InstitutionPayload & {
status?: Status;
},
) {
return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
where: {
@ -141,10 +153,16 @@ export class InstitutionController extends Controller {
@Put("{institutionId}")
@Security("keycloak")
@OperationId("updateInstitution")
async updateInstitution(@Path() institutionId: string, @Body() body: InstitutionPayload) {
async updateInstitution(
@Path() institutionId: string,
@Body()
body: InstitutionPayload & {
status?: "ACTIVE" | "INACTIVE";
},
) {
return await prisma.institution.update({
where: { id: institutionId },
data: body,
data: { ...body, statusOrder: +(body.status === "INACTIVE") },
});
}
@ -155,9 +173,17 @@ export class InstitutionController extends Controller {
return await prisma.$transaction(async (tx) => {
const record = await tx.institution.findFirst({
where: { id: institutionId },
include: {
taskOrder: {
take: 1,
},
},
});
if (!record) throw notFoundError("Institution");
if (record.status !== "CREATED" || record.taskOrder.length > 0) {
throw isUsedError("Institution");
}
return await tx.institution.delete({
where: { id: institutionId },

View file

@ -150,6 +150,8 @@ export class InvoiceController extends Controller {
createdBy: true,
},
orderBy: { createdAt: "asc" },
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.invoice.count({ where }),
]);

View file

@ -54,6 +54,8 @@ export class ReceiptController extends Controller {
},
},
orderBy: { createdAt: "asc" },
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.payment.count({ where }),
]);

View file

@ -466,12 +466,6 @@ export class ServiceController extends Controller {
);
}
}
} else {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Installments validate failed.",
"installmentsValidateFailed",
);
}
const record = await prisma.$transaction(async (tx) => {

View file

@ -1,4 +1,4 @@
import { PayCondition, Prisma, QuotationStatus, Status } from "@prisma/client";
import { PayCondition, Prisma, QuotationStatus, RequestDataStatus, Status } from "@prisma/client";
import config from "../config.json";
import {
Body,
@ -219,16 +219,28 @@ export class QuotationController extends Controller {
code,
payCondition,
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
quotationStatus: forDebitNote ? { notIn: ["Issued", "Expired"] } : status,
quotationStatus: forDebitNote
? { notIn: ["Issued", "Expired", "Accepted", "Canceled"] }
: status,
requestData: hasCancel
? {
some: {
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
OR: [
{
requestDataStatus: RequestDataStatus.Canceled,
requestWork: {
some: { creditNoteId: null },
},
},
},
{
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
},
],
},
}
: undefined,
@ -272,20 +284,33 @@ export class QuotationController extends Controller {
select: {
requestWork: {
where: {
OR: [
{ request: { requestDataStatus: RequestDataStatus.Canceled } },
{ stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } } },
],
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
},
},
},
where: {
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
OR: [
{
requestDataStatus: RequestDataStatus.Canceled,
requestWork: {
some: { creditNoteId: null },
},
},
},
{
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
},
],
quotationId: { in: result.map((v) => v.id) },
},
});

View file

@ -324,12 +324,6 @@ export class RequestListController extends Controller {
};
}
if (cancelOnly) {
statusCondition = {
some: { workStatus: RequestWorkStatus.Canceled },
};
}
if (workStatus && !readyToTask && !cancelOnly) {
statusCondition = {
some: { workStatus },
@ -337,8 +331,18 @@ export class RequestListController extends Controller {
}
const where = {
stepStatus: readyToTask || cancelOnly || workStatus ? statusCondition : undefined,
creditNote: cancelOnly ? null : undefined,
OR: cancelOnly
? [
{
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
{
request: { requestDataStatus: RequestDataStatus.Canceled },
},
]
: undefined,
stepStatus: readyToTask || workStatus ? statusCondition : undefined,
creditNoteId: cancelOnly ? null : undefined,
request: {
id: requestDataId,
requestDataStatus: readyToTask

View file

@ -20,6 +20,7 @@ import {
QuotationStatus,
RequestDataStatus,
RequestWorkStatus,
Status,
TaskOrderStatus,
TaskStatus,
UserTaskStatus,
@ -295,6 +296,10 @@ export class TaskController extends Controller {
"requestWorkMustReady",
);
}
await tx.institution.updateMany({
where: { id: body.institutionId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
const work = await tx.requestWorkStepStatus.findMany({
include: {

View file

@ -34,7 +34,7 @@ import {
setFile,
} from "../utils/minio";
import { notFoundError } from "../utils/error";
import { CreditNotePaybackType, CreditNoteStatus, Prisma } from "@prisma/client";
import { CreditNotePaybackType, CreditNoteStatus, Prisma, RequestDataStatus } from "@prisma/client";
import { queryOrNot } from "../utils/relation";
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
@ -100,7 +100,16 @@ export class CreditNoteController extends Controller {
},
},
} satisfies Prisma.CreditNoteWhereInput;
return await prisma.creditNote.count({ where });
const result = await prisma.creditNote.groupBy({
_count: true,
by: "creditNoteStatus",
where,
});
return result.reduce<Record<string, number>>((a, c) => {
a[c.creditNoteStatus] = c._count;
return a;
}, {});
}
@Get()
@ -138,6 +147,7 @@ export class CreditNoteController extends Controller {
const where = {
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
{
code: { contains: query, mode: "insensitive" },
requestWork: {
some: {
request: {
@ -277,11 +287,10 @@ export class CreditNoteController extends Controller {
id: body.quotationId,
},
},
stepStatus: {
some: {
workStatus: "Canceled",
},
},
OR: [
{ request: { requestDataStatus: RequestDataStatus.Canceled } },
{ stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } } },
],
id: { in: body.requestWorkId },
},
include: {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,150 @@
import { Body, Controller, Get, Post, Request, Route, Security, Tags } from "tsoa";
import prisma from "../db";
import nodemailer from "nodemailer";
import { notFoundError } from "../utils/error";
import { RequestWithLineUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
type SendEmail = {
identityNumber: string;
email: string;
};
type VerificationPayload = {
identityNumber: string;
email: string;
otp: string;
};
let emailTransport: ReturnType<(typeof nodemailer)["createTransport"]>;
@Route("/api/v1/verification")
@Tags("Verification")
export class verificationController extends Controller {
@Get()
@Security("line")
async isRegistered(@Request() req: RequestWithLineUser) {
return !!(await prisma.customerBranch.findFirst({ where: { userId: req.user.sub } }));
}
@Post("/send-otp")
public async sendOTP(@Body() body: SendEmail) {
if (
![
process.env.SMTP_HOST,
process.env.SMTP_PORT,
process.env.SMTP_USER,
process.env.SMTP_PASS,
].every(Boolean)
) {
throw new HttpError(
HttpStatus.PRECONDITION_FAILED,
"SMTP not configured",
"smtpNotConfigured",
);
}
if (!emailTransport) {
emailTransport = nodemailer.createTransport({
host: process.env.SMTP_HOST!,
port: +process.env.SMTP_PORT!,
secure: +process.env.SMTP_PORT! === 465, // true for port 465, false for other ports
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
});
}
const generateOTP = Math.floor(100000 + Math.random() * 900000).toString();
const expiresTime = new Date(Date.now() + 5 * 60 * 1000);
const dataCustomerBranch = await prisma.customerBranch.findFirst({
where: {
email: body.email,
OR: [
{
citizenId: body.identityNumber,
customer: {
customerType: "PERS",
},
},
{
legalPersonNo: body.identityNumber,
customer: {
customerType: "CORP",
},
},
],
},
});
if (!dataCustomerBranch) throw notFoundError("Customer Branch");
await emailTransport.sendMail({
from: process.env.SMTP_USER,
to: body.email,
subject: "Your OTP Code",
text: `Your OTP Code ${generateOTP}`,
html: `<p>Your OTP code is: <strong>${generateOTP}</strong></p>`,
});
await prisma.customerBranch.update({
where: {
id: dataCustomerBranch.id,
},
data: {
otpCode: generateOTP,
otpExpires: expiresTime,
},
});
return { message: "OTP sent successfully" };
}
@Post("/verify-otp")
@Security("line")
public async verifyOTP(@Request() req: RequestWithLineUser, @Body() body: VerificationPayload) {
const customerBranch = await prisma.customerBranch.findFirst({
where: {
email: body.email,
OR: [
{
citizenId: body.identityNumber,
customer: {
customerType: "PERS",
},
},
{
legalPersonNo: body.identityNumber,
customer: {
customerType: "CORP",
},
},
],
},
});
if (!customerBranch) throw notFoundError("Customer Branch");
if (
customerBranch.otpCode &&
customerBranch.otpCode === body.otp &&
customerBranch.otpExpires &&
customerBranch.otpExpires >= new Date()
) {
const dataCustomer = await prisma.customerBranch.update({
where: {
id: customerBranch.id,
},
data: {
userId: req.user.sub,
},
});
return dataCustomer;
}
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid OTP", "invalidOTP");
}
}

View file

@ -0,0 +1,183 @@
import { Body, Controller, Get, Post, Query, Response, Route, Tags } from "tsoa";
import prisma from "../db";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
dayjs.extend(utc);
dayjs.extend(timezone);
interface WebhookPayload {
destination?: string;
events: Array<{
mode?: string;
deliveryContext?: Record<string, any>;
webhookEventId?: string;
type: string;
replyToken: string;
source: {
userId: string;
type: string;
};
timestamp: number;
message: {
id?: string;
type: string;
text?: string;
quoteToken?: string;
stickerId?: string;
packageId?: string;
stickerResourceType?: string;
keywords?: string[];
emojis?: string[] | { productId: string; emojiId: string; index: number; length: number }[];
};
}>;
}
interface accessToken {
code: string;
state: string;
}
@Route("api/v1/webhook")
@Tags("Webhook")
export class WebHookController extends Controller {
async #getLineToken() {
if (!process.env.LINE_MESSAGING_API_TOKEN) {
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "NOT IMPLEMENTED", "notImplemented");
}
return process.env.LINE_MESSAGING_API_TOKEN;
}
@Post()
@Response(200, "Webhook received successfully")
public async receiveWebhook(@Body() payload: WebhookPayload) {
const token = await this.#getLineToken();
if (!payload || !payload.events || !Array.isArray(payload.events)) {
this.setStatus(400);
return { message: "Invalid payload structure" };
}
if (payload.events.length > 0) {
const userIdLine = payload.events[0]?.source?.userId;
const dataNow = dayjs().tz("Asia/Bangkok").startOf("day");
// const dataUser = await prisma.customerBranch.findFirst({
// where:{
// userId:userIdLine
// }
// })
const dataEmployee = await prisma.employeePassport.findMany({
select: {
firstName: true,
firstNameEN: true,
lastName: true,
lastNameEN: true,
employeeId: true,
expireDate: true,
employee: {
select: {
firstName: true,
lastName: true,
customerBranch: {
select: {
firstName: true,
firstNameEN: true,
lastName: true,
lastNameEN: true,
customerName: true,
customer: {
select: {
customerType: true,
registeredBranch: {
select: {
telephoneNo: true,
},
},
},
},
},
},
},
},
},
where: {
expireDate: {
lt: dataNow.add(30, "day").toDate(),
},
},
orderBy: {
expireDate: "asc",
},
});
if (payload?.events[0]?.message) {
const message = payload.events[0].message.text;
if (message === "เมนูหลัก > ข้อความ") {
const dataUser = userIdLine;
const textHead = "JWS ALERT:";
let textData = "";
if (dataEmployee.length > 0) {
const customerName =
dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
const telephoneNo =
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
"ไม่ระบุ";
const textEmployer = `เรียน คุณ${customerName}`;
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
const textExpDate =
"🔹 กรุณาตรวจสอบและดำเนินการต่ออายุภายในวันที่กำหนด เพื่อป้องกันปัญหาด้านเอกสารหรือการเดินทางที่อาจเกิดขึ้น";
const textAlert3 = "หากดำเนินการเรียบร้อยแล้ว กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
let textFooter = `📞 สอบถามข้อมูลเพิ่มเติม: ${telephoneNo}`;
const textEmployees = dataEmployee
.map((item, index) => {
const dateFormat =
dayjs(item.expireDate).format("DD/MM/") + (dayjs(item.expireDate).year() + 543);
const diffDate = dayjs(item.expireDate).diff(dayjs(), "day");
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n https://taii-cmm.case-collection.com/api/v1/line/employee/${item.employeeId}`;
})
.join("\n");
textData = `${textHead}\n\n${textEmployer}\n\n${textAlert}\n${textEmployees}\n${textAlert2}\n\n${textExpDate}\n\n${textAlert3}\n\n${textFooter}`;
} else {
textData = `${textHead}\n\nขออภัย ไม่พบข้อมูลหนังสือเดินทางที่มีกำหนดหมดอายุภายใน 30 วันข้างหน้า 🙏`;
}
const data = {
to: dataUser,
messages: [
{
type: "text",
text: textData,
},
],
};
await fetch("https://api.line.me/v2/bot/message/push", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}
}
}
return { message: "Webhook received successfully" };
}
}

View file

@ -11,3 +11,15 @@ export type RequestWithUser = Request & {
roles: string[];
};
};
export type RequestWithLineUser = Request & {
user: {
iss: string;
sub: string;
aud: string;
exp: number;
iat: number;
name: string;
picture: string;
};
};

View file

@ -0,0 +1,47 @@
import Express from "express";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
export async function lineAuth(request: Express.Request) {
const data = new URLSearchParams();
const token = request.headers["authorization"];
if (!process.env.LINE_CLIENT_ID) {
console.warn("Line related endpoint was called but LINE_CLIENT_ID not set.");
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "NOT IMPLEMENTED", "notImplemented");
}
const LINE_CLIENT_ID = process.env.LINE_CLIENT_ID;
if (!token || typeof token !== "string") {
throw new HttpError(
HttpStatus.UNAUTHORIZED,
"authorization data not found.",
"authDataNotFound",
);
}
data.append("id_token", token);
data.append("client_id", LINE_CLIENT_ID);
const dataUser = await fetch("https://api.line.me/oauth2/v2.1/verify", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: data.toString(),
});
if (!dataUser)
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Error authentication service.",
"authFailedFatal",
);
if (!dataUser.ok) {
throw new HttpError(HttpStatus.UNAUTHORIZED, "Unauthorized.", "authFailed");
}
return await dataUser.json();
}

View file

@ -2,6 +2,7 @@ import Express from "express";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { keycloakAuth } from "./auth-provider/keycloak";
import { lineAuth } from "./auth-provider/line";
export async function expressAuthentication(
request: Express.Request,
@ -17,6 +18,9 @@ export async function expressAuthentication(
request.app.locals.logData.userName = authData.name;
request.app.locals.logData.userId = authData.sub;
return authData;
case "line":
const authLineData = await lineAuth(request);
return authLineData;
default:
throw new HttpError(
HttpStatus.NOT_IMPLEMENTED,

View file

@ -11,7 +11,7 @@ export function relationError(name: string) {
export function notFoundError(name: string) {
return new HttpError(
HttpStatus.BAD_REQUEST,
HttpStatus.NOT_FOUND,
`${name} cannot be found.`,
`${name.charAt(0).toLowerCase() + name.replaceAll(" ", "").slice(1)}NotFound`,
);