Compare commits

...

93 Commits

Author SHA1 Message Date
e06642225a feat: working on creating parent card 2025-08-14 14:40:08 +03:00
c06086f899 Merge pull request #30 from HamzaSha1/dev
Dev
2025-08-11 18:22:34 +03:00
e775561a89 Merge pull request #29 from HamzaSha1/main
Merge main into dev
2025-08-11 18:21:52 +03:00
241f1ce427 Merge branch 'dev' into main 2025-08-11 18:21:43 +03:00
d883bd2d9a Update junior.repository.ts 2025-08-11 18:19:57 +03:00
cd800ff8b8 Update customer.repository.ts 2025-08-11 18:18:48 +03:00
05a9f04ac8 fix: fix import in migration index 2025-08-11 16:23:19 +03:00
dcc9077392 fix: rename migration timestamp 2025-08-11 16:22:20 +03:00
681d1e5791 fix: fix seed default avatar migration 2025-08-11 16:16:26 +03:00
bf505a65bf fix: fix invalid imports 2025-08-11 16:13:53 +03:00
6bf32d27c7 Merge branch 'dev' of github.com:HamzaSha1/zod-backend into dev 2025-08-11 15:33:39 +03:00
ac63d4cdc7 refactor: refactor the code 2025-08-11 15:33:32 +03:00
150027fb71 Merge branch 'main' into dev 2025-08-11 15:25:16 +03:00
e8ee74d0d7 refactor: remove unsed code 2025-08-11 15:15:41 +03:00
5f2e06edf9 Merge pull request #28 from HamzaSha1/feat/neoleap-integration
Feat/neoleap integration
2025-08-10 16:16:32 +03:00
99ad17f0f9 feat: add change password api 2025-08-07 15:25:45 +03:00
ee7b365527 feat: kyc process 2025-08-07 14:23:33 +03:00
275984954e feat: working on edit profile ticket 2025-08-05 17:53:38 +03:00
6f7fb2bdcd Merge pull request #27 from HamzaSha1/feat/neoleap-integration
Merge neoleap-integration into dev
2025-08-03 16:24:46 +03:00
1e2b859b92 feat: finish generating signed url for document upload flow 2025-08-03 16:18:06 +03:00
4cc52a1c07 fix: add swagger doc to verify otp api 2025-08-03 14:50:04 +03:00
7461af20dd feat: edit forget password flow 2025-08-03 14:48:14 +03:00
f65a7d2933 feat: generate upload signed url for oci 2025-08-03 14:21:14 +03:00
fce720237f feat: add vpan to card entity 2025-08-03 11:53:16 +03:00
5e0a4e6bd1 feat: fix update card status webhook 2025-07-31 14:42:06 +03:00
f9776e60cf fix: save transaction file 2025-07-31 14:11:53 +03:00
7e63abb2fb feat: add-account-details 2025-07-31 14:07:01 +03:00
a245545811 feat: add login and forget password and refactor code 2025-07-30 15:40:40 +03:00
4cb5814cd3 fix: organize migrations 2025-07-30 14:18:10 +03:00
9e06ea4d71 Merge branch 'dev' into feat/neoleap-integration 2025-07-30 14:09:00 +03:00
cff87c4ecd rollup migrations into one 2025-07-30 13:08:31 +03:00
1541c374ed feat: fix swagger examples 2025-07-27 13:26:21 +03:00
c493bd57e1 feat: onboarding signup journey 2025-07-27 13:15:54 +03:00
bf43e62b17 feat: handle card status changed webhook 2025-07-21 15:30:55 +03:00
5a780eeb17 feat/working on update card control 2025-07-14 11:57:51 +03:00
038b8ef6e3 feat: finish working on account transaction webhook 2025-07-09 13:31:08 +03:00
3b3f8c0104 fix: remove host from request 2025-07-07 16:34:45 +03:00
2770cf8774 fix:fix card migration 2025-07-07 12:06:01 +03:00
bea3ccfbbc Merge branch 'waiting-list' into feat/neoleap-integration 2025-07-06 16:45:37 +03:00
492e538eb8 feat: send request via gateway 2025-07-06 16:44:23 +03:00
d3057beb54 feat: add transaction, card , and account entities 2025-07-02 18:42:38 +03:00
19fa53c981 fix: fix apple client audience 2025-06-17 11:39:51 +03:00
d2cc02fb60 fix: localizze error messages 2025-06-17 09:43:36 +03:00
4cbbfd8136 Merge branch 'waiting-list' into feat/neoleap-integration 2025-06-11 11:15:10 +03:00
6c859a25d2 feat: handle oauth2 login 2025-06-04 14:52:09 +03:00
d1a6d3e715 feat: add test controller for integartion 2025-06-04 10:04:45 +03:00
1ea1f42169 feat: finish create and inquire application api and handle response and errors 2025-06-03 14:51:36 +03:00
d4fe3b3fc3 feat: finish working on mocking inquire application api 2025-05-26 16:34:09 +03:00
b44bc5d5cc fix: localize customer already exist message 2025-05-26 15:24:20 +03:00
9aa6c487ed Merge branch 'waiting-list' into feat/neoleap-integration 2025-05-26 12:11:53 +03:00
42e4d75d70 feat: add country iso enum 2025-05-26 12:10:09 +03:00
a358cd2e7a feat: add neoleap service and mock create application api 2025-05-26 12:04:00 +03:00
641a665beb Merge branch 'waiting-list' into feat/neoleap-integration 2025-05-21 09:59:18 +03:00
49326e983f feat: handle new registration flow 2025-05-19 17:00:32 +03:00
881d88c8d8 feat: add customer details to customer entity 2025-05-19 14:16:18 +03:00
35ab3df7c1 feat: update create junior payload and add new document type 2025-05-14 14:53:22 +03:00
cbade0a87d feat: blacklist refresh tokens 2025-04-09 16:20:29 +03:00
4c6ef17525 refactor: remove login by password and biometric 2025-04-09 15:07:48 +03:00
ffca6996fd feat: styling email template 2025-04-06 13:33:57 +03:00
a3f88c774c feat: handle notification using redis 2025-03-27 12:33:56 +03:00
ec38b82a7b feat: add waiting number and handle resent otp 2025-03-16 11:34:08 +03:00
9b5f863577 add login flow for waiting list demo app 2025-03-04 14:42:02 +03:00
54ce5b022d feat: adding enhancment for zod admin portal kyc and add customer additional fields 2025-03-02 10:49:58 +03:00
dae9cb6323 refactor: handle kyc journey for customers 2025-02-20 16:18:06 +03:00
270753cfd7 fix: fix domain name for apple auth 2025-01-16 13:42:52 +03:00
6b1cb3a84e fix: fix apibase url 2025-01-16 13:38:59 +03:00
ebd4b293e9 feat: client testing api , add ouath2 2025-01-16 13:35:22 +03:00
87bb1a2709 feat: add apple login 2025-01-16 12:17:10 +03:00
663e8972c4 feat: client test app, add apple login 2025-01-15 14:37:53 +03:00
8ff9f921e8 feat: add localization for request dto validations and add accept lanuage header 2025-01-14 14:59:01 +03:00
6d2d2b558a feat: add localization to expection messages 2025-01-14 12:39:19 +03:00
5aa3d3774d refactor: enhance customer creation code 2025-01-13 16:42:27 +03:00
221f3bae4f feat: add find task by id endpoint 2025-01-13 14:15:29 +03:00
62621c1a15 feat: add validation for documents 2025-01-13 11:43:28 +03:00
756e947c8a feat: handle google login 2025-01-09 15:14:09 +03:00
db02a28b4d hotfix: fix completed task filter 2025-01-07 14:18:08 +03:00
afc087ff08 api client for testing 2025-01-07 01:46:58 -05:00
ee433a5c8c fix: remove send email endpoint 2025-01-06 16:47:43 +03:00
3ab0f179d8 feat: add smtp and fix dynamic link 2025-01-06 16:46:35 +03:00
25ef549417 feat: prefix all apis 2025-01-05 14:46:49 +03:00
084d39096c feat:integration brancjio dynamic links to junior qr code registration 2025-01-05 13:15:57 +03:00
eca84b4e75 fix: general fixes on date types and some typos 2025-01-05 10:38:19 +03:00
aefa866ae7 feat: add money request cronjob, and edit money request entity 2025-01-02 11:22:33 +03:00
557ef4cd33 Merge pull request #26 from HamzaSha1/feat/cron-lock
Feat/cron lock
2024-12-30 16:18:06 +03:00
eea6302dda Merge branch 'mvp-1' of github.com:HamzaSha1/zod-backend into feat/cron-lock 2024-12-30 16:10:28 +03:00
c0fafd3f7c feat: add cron job for allowance and implement cron lock 2024-12-30 16:10:19 +03:00
f7290419d2 Merge pull request #25 from HamzaSha1/feat/add-loggers
feat: add loggers to all services
2024-12-30 10:44:07 +03:00
0fd2066c4a feat: add loggers to all services 2024-12-30 10:35:36 +03:00
cb54311a7b Merge pull request #24 from HamzaSha1/refactor/seperate-auth-and-user
refactor: seperate user and auth modules
2024-12-29 14:30:15 +03:00
ca71632755 refactor: sepeare user and auth modules 2024-12-29 14:17:39 +03:00
ebf335eabd Merge pull request #23 from HamzaSha1/feat/use-event-emitter
feat: handle notification async using event emitter
2024-12-29 11:53:23 +03:00
f383f6d14d feat: handle notification async using event emitter 2024-12-29 11:44:51 +03:00
5663a287f9 Merge pull request #22 from HamzaSha1/feat/mobile-notification
feat: sms using twillio
2024-12-29 10:00:24 +03:00
350 changed files with 7617 additions and 4924 deletions

View File

@ -28,4 +28,13 @@ MAIL_HOST=smtp.gmail.com
MAIL_USER=aahalhmad@gmail.com
MAIL_PASSWORD=
MAIL_PORT=587
MAIL_FROM=UBA
MAIL_FROM=UBA
BRANCH_IO_URL=https://api2.branch.io/v1/url
BRANCH_IO_KEY=
ZOD_BASE_URL=http://localhost:5001
ANDROID_PACKAGE_NAME=com.zod
IOS_PACKAGE_NAME=com.zod
ANDRIOD_JUNIOR_DEEPLINK_PATH=zodbank://juniors/qr-code/validate
IOS_JUNIOR_DEEPLINK_PATH=zodbank://juniors/qr-code/validate

View File

@ -29,7 +29,7 @@ module.exports = {
'require-await': ['error'],
'no-console': ['error'],
'no-multi-assign': ['error'],
'no-magic-numbers': ['error', { ignoreArrayIndexes: true }],
'no-magic-numbers': ['error', { ignoreArrayIndexes: true}],
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1, maxBOF: 0 }],
'max-len': [
'error',

2
.gitignore vendored
View File

@ -53,3 +53,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
zod-certs

View File

@ -10,6 +10,8 @@
"include": "config",
"exclude": "**/*.md"
},
{ "include": "common/modules/**/templates/**/*", "watchAssets": true },
{ "include": "common/modules/neoleap/zod-certs" },
"i18n",
"files"
]

187
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@nestjs/microservices": "^10.4.7",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.8",
"@nestjs/schedule": "^4.1.2",
"@nestjs/swagger": "^8.0.5",
"@nestjs/terminus": "^10.2.3",
"@nestjs/throttler": "^6.2.1",
@ -32,10 +33,12 @@
"cacheable": "^1.8.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"decimal.js": "^10.6.0",
"firebase-admin": "^13.0.2",
"google-libphonenumber": "^3.2.39",
"handlebars": "^4.7.8",
"ioredis": "^5.4.1",
"handlebars-layouts": "^3.1.4",
"jwk-to-pem": "^2.0.7",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"nestjs-i18n": "^10.4.9",
@ -64,6 +67,7 @@
"@types/express": "^5.0.0",
"@types/google-libphonenumber": "^7.4.30",
"@types/jest": "^29.5.2",
"@types/jwk-to-pem": "^2.0.3",
"@types/lodash": "^4.17.13",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
@ -1184,7 +1188,9 @@
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
@ -2198,6 +2204,33 @@
"@nestjs/core": "^10.0.0"
}
},
"node_modules/@nestjs/schedule": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz",
"integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==",
"license": "MIT",
"dependencies": {
"cron": "3.2.1",
"uuid": "11.0.3"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
}
},
"node_modules/@nestjs/schedule/node_modules/uuid": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/@nestjs/schematics": {
"version": "10.2.3",
"dev": true,
@ -2830,6 +2863,13 @@
"version": "2.0.0",
"license": "MIT"
},
"node_modules/@types/jwk-to-pem": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.3.tgz",
"integrity": "sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.13",
"dev": true,
@ -2842,6 +2882,12 @@
"license": "MIT",
"optional": true
},
"node_modules/@types/luxon": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"dev": true,
@ -3754,6 +3800,18 @@
"safer-buffer": "~2.1.0"
}
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/assert-never": {
"version": "1.3.0",
"license": "MIT",
@ -4021,6 +4079,12 @@
"node": ">= 6"
}
},
"node_modules/bn.js": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"license": "MIT",
@ -4121,6 +4185,12 @@
"node": ">=8"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.24.2",
"dev": true,
@ -5005,6 +5075,16 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/cron": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz",
"integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==",
"license": "MIT",
"dependencies": {
"@types/luxon": "~3.4.0",
"luxon": "~3.5.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.5",
"license": "MIT",
@ -5088,6 +5168,12 @@
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/dedent": {
"version": "1.5.3",
"dev": true,
@ -5162,6 +5248,8 @@
"node_modules/denque": {
"version": "2.1.0",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10"
}
@ -5470,6 +5558,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/elliptic": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/emitter-listener": {
"version": "1.1.2",
"license": "BSD-2-Clause",
@ -6888,6 +6991,15 @@
"uglify-js": "^3.1.4"
}
},
"node_modules/handlebars-layouts": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/handlebars-layouts/-/handlebars-layouts-3.1.4.tgz",
"integrity": "sha512-2llBmvnj8ueOfxNHdRzJOcgalzZjYVd9+WAl93kPYmlX4WGx7FTHTzNxhK+i9YKY2OSjzfehgpLiIwP/OJr6tw==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/handlebars/node_modules/source-map": {
"version": "0.6.1",
"license": "BSD-3-Clause",
@ -6950,6 +7062,16 @@
"version": "2.0.1",
"license": "ISC"
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"license": "MIT",
@ -6987,6 +7109,17 @@
"node": "*"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"license": "MIT",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/hookified": {
"version": "1.5.1",
"license": "MIT"
@ -7268,6 +7401,8 @@
"node_modules/ioredis": {
"version": "5.4.1",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
@ -8459,6 +8594,17 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwk-to-pem": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz",
"integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==",
"license": "Apache-2.0",
"dependencies": {
"asn1.js": "^5.3.0",
"elliptic": "^6.6.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwks-rsa": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz",
@ -8981,7 +9127,9 @@
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/lodash.includes": {
"version": "4.3.0",
@ -8989,7 +9137,9 @@
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
@ -9238,6 +9388,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/luxon": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.8",
"dev": true,
@ -9479,6 +9638,18 @@
"node": ">=6"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
"license": "MIT"
},
"node_modules/minimatch": {
"version": "9.0.5",
"license": "ISC",
@ -15563,6 +15734,8 @@
"node_modules/redis-errors": {
"version": "1.2.0",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=4"
}
@ -15570,6 +15743,8 @@
"node_modules/redis-parser": {
"version": "3.0.0",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"redis-errors": "^1.0.0"
},
@ -16354,7 +16529,9 @@
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/statuses": {
"version": "2.0.1",

View File

@ -23,7 +23,8 @@
"migration:generate": "npm run typeorm:cli-d migration:generate",
"migration:create": "npm run typeorm:cli migration:create",
"migration:up": "npm run typeorm:cli-d migration:run",
"migration:down": "npm run typeorm:cli-d migration:revert"
"migration:down": "npm run typeorm:cli-d migration:revert",
"seed": "TS_NODE_PROJECT=tsconfig.json ts-node -r tsconfig-paths/register src/scripts/seed.ts"
},
"dependencies": {
"@abdalhamid/hello": "^2.0.0",
@ -39,6 +40,7 @@
"@nestjs/microservices": "^10.4.7",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.8",
"@nestjs/schedule": "^4.1.2",
"@nestjs/swagger": "^8.0.5",
"@nestjs/terminus": "^10.2.3",
"@nestjs/throttler": "^6.2.1",
@ -49,10 +51,12 @@
"cacheable": "^1.8.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"decimal.js": "^10.6.0",
"firebase-admin": "^13.0.2",
"google-libphonenumber": "^3.2.39",
"handlebars": "^4.7.8",
"ioredis": "^5.4.1",
"handlebars-layouts": "^3.1.4",
"jwk-to-pem": "^2.0.7",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"nestjs-i18n": "^10.4.9",
@ -81,6 +85,7 @@
"@types/express": "^5.0.0",
"@types/google-libphonenumber": "^7.4.30",
"@types/jest": "^29.5.2",
"@types/jwk-to-pem": "^2.0.3",
"@types/lodash": "^4.17.13",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",

View File

@ -1,20 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JuniorModule } from '~/junior/junior.module';
import { AllowanceChangeRequestController, AllowancesController } from './controllers';
import { Allowance, AllowanceChangeRequest } from './entities';
import { AllowanceChangeRequestsRepository, AllowancesRepository } from './repositories';
import { AllowanceChangeRequestsService, AllowancesService } from './services';
@Module({
controllers: [AllowancesController, AllowanceChangeRequestController],
imports: [TypeOrmModule.forFeature([Allowance, AllowanceChangeRequest]), JuniorModule],
providers: [
AllowancesService,
AllowancesRepository,
AllowanceChangeRequestsService,
AllowanceChangeRequestsRepository,
],
exports: [],
})
export class AllowanceModule {}

View File

@ -1,80 +0,0 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Roles } from '~/auth/enums';
import { IJwtPayload } from '~/auth/interfaces';
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
import { RolesGuard } from '~/common/guards';
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
import { PageOptionsRequestDto } from '~/core/dtos';
import { CustomParseUUIDPipe } from '~/core/pipes';
import { ResponseFactory } from '~/core/utils';
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
import { AllowanceChangeRequestResponseDto } from '../dtos/response';
import { AllowanceChangeRequestsService } from '../services';
@Controller('allowance-change-requests')
@ApiTags('Allowance Change Requests')
@ApiBearerAuth()
export class AllowanceChangeRequestController {
constructor(private readonly allowanceChangeRequestsService: AllowanceChangeRequestsService) {}
@Post()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.JUNIOR)
@HttpCode(HttpStatus.NO_CONTENT)
requestAllowanceChange(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceChangeRequestDto) {
return this.allowanceChangeRequestsService.createAllowanceChangeRequest(sub, body);
}
@Get()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataPageResponse(AllowanceChangeRequestResponseDto)
async findAllowanceChangeRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
const [requests, itemCount] = await this.allowanceChangeRequestsService.findAllowanceChangeRequests(sub, query);
return ResponseFactory.dataPage(
requests.map((request) => new AllowanceChangeRequestResponseDto(request)),
{
itemCount,
page: query.page,
size: query.size,
},
);
}
@Get('/:changeRequestId')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(AllowanceChangeRequestResponseDto)
async findAllowanceChangeRequestById(
@AuthenticatedUser() { sub }: IJwtPayload,
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
) {
const request = await this.allowanceChangeRequestsService.findAllowanceChangeRequestById(sub, changeRequestId);
return ResponseFactory.data(new AllowanceChangeRequestResponseDto(request));
}
@Patch(':changeRequestId/approve')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@HttpCode(HttpStatus.NO_CONTENT)
approveAllowanceChangeRequest(
@AuthenticatedUser() { sub }: IJwtPayload,
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
) {
return this.allowanceChangeRequestsService.approveAllowanceChangeRequest(sub, changeRequestId);
}
@Patch(':changeRequestId/reject')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@HttpCode(HttpStatus.NO_CONTENT)
rejectAllowanceChangeRequest(
@AuthenticatedUser() { sub }: IJwtPayload,
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
) {
return this.allowanceChangeRequestsService.rejectAllowanceChangeRequest(sub, changeRequestId);
}
}

View File

@ -1,72 +0,0 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Roles } from '~/auth/enums';
import { IJwtPayload } from '~/auth/interfaces';
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
import { RolesGuard } from '~/common/guards';
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
import { PageOptionsRequestDto } from '~/core/dtos';
import { CustomParseUUIDPipe } from '~/core/pipes';
import { ResponseFactory } from '~/core/utils';
import { CreateAllowanceRequestDto } from '../dtos/request';
import { AllowanceResponseDto } from '../dtos/response';
import { AllowancesService } from '../services';
@Controller('allowances')
@ApiTags('Allowances')
@ApiBearerAuth()
export class AllowancesController {
constructor(private readonly allowancesService: AllowancesService) {}
@Post()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(AllowanceResponseDto)
async createAllowance(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceRequestDto) {
const allowance = await this.allowancesService.createAllowance(sub, body);
return ResponseFactory.data(new AllowanceResponseDto(allowance));
}
@Get()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataPageResponse(AllowanceResponseDto)
async findAllowances(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
const [allowances, itemCount] = await this.allowancesService.findAllowances(sub, query);
return ResponseFactory.dataPage(
allowances.map((allowance) => new AllowanceResponseDto(allowance)),
{
itemCount,
page: query.page,
size: query.size,
},
);
}
@Get(':allowanceId')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(AllowanceResponseDto)
async findAllowanceById(
@AuthenticatedUser() { sub }: IJwtPayload,
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
) {
const allowance = await this.allowancesService.findAllowanceById(allowanceId, sub);
return ResponseFactory.data(new AllowanceResponseDto(allowance));
}
@Delete(':allowanceId')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(AllowanceResponseDto)
@HttpCode(HttpStatus.NO_CONTENT)
deleteAllowance(
@AuthenticatedUser() { sub }: IJwtPayload,
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
) {
return this.allowancesService.deleteAllowance(sub, allowanceId);
}
}

View File

@ -1,2 +0,0 @@
export * from './allowance-change-request.controller';
export * from './allowances.controller';

View File

@ -1,28 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class CreateAllowanceChangeRequestDto {
@ApiProperty({ example: 'I want to change the amount of the allowance' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowanceChangeRequest.reason' }) })
@IsNotEmpty({
message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowanceChangeRequest.reason' }),
})
reason!: string;
@ApiProperty({ example: 100 })
@IsNumber(
{},
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowanceChangeRequest.amount' }) },
)
@IsPositive({
message: i18n('validation.IsPositive', { path: 'general', property: 'allowanceChangeRequest.amount' }),
})
amount!: number;
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
@IsUUID('4', {
message: i18n('validation.IsUUID', { path: 'general', property: 'allowanceChangeRequest.allowanceId' }),
})
allowanceId!: string;
}

View File

@ -1,52 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID, ValidateIf } from 'class-validator';
import moment from 'moment';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
export class CreateAllowanceRequestDto {
@ApiProperty({ example: 'Allowance name' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowance.name' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowance.name' }) })
name!: string;
@ApiProperty({ example: 100 })
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.amount' }) })
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
amount!: number;
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
@IsEnum(AllowanceFrequency, {
message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.frequency' }),
})
frequency!: AllowanceFrequency;
@ApiProperty({ example: AllowanceType.BY_END_DATE })
@IsEnum(AllowanceType, { message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.type' }) })
type!: AllowanceType;
@ApiProperty({ example: new Date() })
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.startDate' }) })
@Transform(({ value }) => moment(value).startOf('day').toDate())
startDate!: Date;
@ApiProperty({ example: new Date() })
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.endDate' }) })
@Transform(({ value }) => moment(value).endOf('day').toDate())
@ValidateIf((o) => o.type === AllowanceType.BY_END_DATE)
endDate?: Date;
@ApiProperty({ example: 10 })
@IsNumber(
{},
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.numberOfTransactions' }) },
)
@IsInt({ message: i18n('validation.IsInt', { path: 'general', property: 'allowance.amount' }) })
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
@ValidateIf((o) => o.type === AllowanceType.BY_COUNT)
numberOfTransactions?: number;
@ApiProperty({ example: 'e7b1b3b4-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'allowance.juniorId' }) })
juniorId!: string;
}

View File

@ -1,2 +0,0 @@
export * from './create-allowance-change.request.dto';
export * from './create-allowance.request.dto';

View File

@ -1,45 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { AllowanceChangeRequest } from '~/allowance/entities';
import { AllowanceChangeRequestStatus } from '~/allowance/enums';
import { JuniorResponseDto } from '~/junior/dtos/response';
export class AllowanceChangeRequestResponseDto {
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
id!: string;
@ApiProperty({ example: AllowanceChangeRequestStatus.APPROVED })
status!: AllowanceChangeRequestStatus;
@ApiProperty({ example: 'Allowance name' })
name!: string;
@ApiProperty({ example: '100' })
oldAmount!: number;
@ApiProperty({ example: '200' })
newAmount!: number;
@ApiProperty({ example: 'Some reason' })
reason!: string;
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
allowanceId!: string;
@ApiProperty({ type: JuniorResponseDto })
junior!: JuniorResponseDto;
@ApiProperty({ example: new Date() })
createdAt!: Date;
constructor(allowanceChangeRequest: AllowanceChangeRequest) {
this.id = allowanceChangeRequest.id;
this.status = allowanceChangeRequest.status;
this.name = allowanceChangeRequest.allowance.name;
this.oldAmount = allowanceChangeRequest.allowance.amount;
this.newAmount = allowanceChangeRequest.amount;
this.reason = allowanceChangeRequest.reason;
this.allowanceId = allowanceChangeRequest.allowanceId;
this.junior = new JuniorResponseDto(allowanceChangeRequest.allowance.junior);
this.createdAt = allowanceChangeRequest.createdAt;
}
}

View File

@ -1,53 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Allowance } from '~/allowance/entities';
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
import { JuniorResponseDto } from '~/junior/dtos/response';
export class AllowanceResponseDto {
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
id!: string;
@ApiProperty({ example: 'Allowance name' })
name!: string;
@ApiProperty({ example: 100 })
amount!: number;
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
frequency!: AllowanceFrequency;
@ApiProperty({ example: AllowanceType.BY_END_DATE })
type!: AllowanceType;
@ApiProperty({ example: new Date() })
startDate!: Date;
@ApiProperty({ example: new Date() })
endDate?: Date;
@ApiProperty({ example: 10 })
numberOfTransactions?: number;
@ApiProperty({ type: JuniorResponseDto })
junior!: JuniorResponseDto;
@ApiProperty({ example: new Date() })
createdAt!: Date;
@ApiProperty({ example: new Date() })
updatedAt!: Date;
constructor(allowance: Allowance) {
this.id = allowance.id;
this.name = allowance.name;
this.amount = allowance.amount;
this.frequency = allowance.frequency;
this.type = allowance.type;
this.startDate = allowance.startDate;
this.endDate = allowance.endDate;
this.numberOfTransactions = allowance.numberOfTransactions;
this.junior = new JuniorResponseDto(allowance.junior);
this.createdAt = allowance.createdAt;
this.updatedAt = allowance.updatedAt;
}
}

View File

@ -1,2 +0,0 @@
export * from './allowance-change-request.response.dto';
export * from './allowance.response.dto';

View File

@ -1,45 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { AllowanceChangeRequestStatus } from '../enums';
import { Allowance } from './allowance.entity';
@Entity('allowance_change_requests')
export class AllowanceChangeRequest {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'text', name: 'reason' })
reason!: string;
@Column({
type: 'decimal',
precision: 10,
scale: 2,
name: 'amount',
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
})
amount!: number;
@Column({ type: 'varchar', length: 255, name: 'status', default: AllowanceChangeRequestStatus.PENDING })
status!: AllowanceChangeRequestStatus;
@Column({ type: 'uuid', name: 'allowance_id' })
allowanceId!: string;
@ManyToOne(() => Allowance, (allowance) => allowance.changeRequests)
@JoinColumn({ name: 'allowance_id' })
allowance!: Allowance;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
}

View File

@ -1,74 +0,0 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { Junior } from '~/junior/entities';
import { AllowanceFrequency, AllowanceType } from '../enums';
import { AllowanceChangeRequest } from './allowance-change-request.entity';
@Entity('allowances')
export class Allowance {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar', length: 255, name: 'name' })
name!: string;
@Column({
type: 'decimal',
precision: 10,
scale: 2,
name: 'amount',
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
})
amount!: number;
@Column({ type: 'varchar', length: 255, name: 'frequency' })
frequency!: AllowanceFrequency;
@Column({ type: 'varchar', length: 255, name: 'type' })
type!: AllowanceType;
@Column({ type: 'timestamp with time zone', name: 'start_date' })
startDate!: Date;
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
endDate?: Date;
@Column({ type: 'int', name: 'number_of_transactions', nullable: true })
numberOfTransactions?: number;
@Column({ type: 'uuid', name: 'guardian_id' })
guardianId!: string;
@Column({ type: 'uuid', name: 'junior_id' })
juniorId!: string;
@ManyToOne(() => Guardian, (guardian) => guardian.allowances)
@JoinColumn({ name: 'guardian_id' })
guardian!: Guardian;
@ManyToOne(() => Junior, (junior) => junior.allowances)
@JoinColumn({ name: 'junior_id' })
junior!: Junior;
@OneToMany(() => AllowanceChangeRequest, (changeRequest) => changeRequest.allowance)
changeRequests!: AllowanceChangeRequest[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
deletedAt?: Date;
}

View File

@ -1,2 +0,0 @@
export * from './allowance-change-request.entity';
export * from './allowance.entity';

View File

@ -1,5 +0,0 @@
export enum AllowanceChangeRequestStatus {
PENDING = 'PENDING',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
}

View File

@ -1,5 +0,0 @@
export enum AllowanceFrequency {
DAILY = 'DAILY',
WEEKLY = 'WEEKLY',
MONTHLY = 'MONTHLY',
}

View File

@ -1,4 +0,0 @@
export enum AllowanceType {
BY_END_DATE = 'BY_END_DATE',
BY_COUNT = 'BY_COUNT',
}

View File

@ -1,3 +0,0 @@
export * from './allowance-change-request-status.enum';
export * from './allowance-frequency.enum';
export * from './allowance-type.enum';

View File

@ -1,50 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { PageOptionsRequestDto } from '~/core/dtos';
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
import { AllowanceChangeRequest } from '../entities';
import { AllowanceChangeRequestStatus } from '../enums';
const ONE = 1;
@Injectable()
export class AllowanceChangeRequestsRepository {
constructor(
@InjectRepository(AllowanceChangeRequest)
private readonly allowanceChangeRequestsRepository: Repository<AllowanceChangeRequest>,
) {}
createAllowanceChangeRequest(allowanceId: string, body: CreateAllowanceChangeRequestDto) {
return this.allowanceChangeRequestsRepository.save(
this.allowanceChangeRequestsRepository.create({
allowanceId,
amount: body.amount,
reason: body.reason,
}),
);
}
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
const relations = withRelations
? ['allowance', 'allowance.junior', 'allowance.junior.customer', 'allowance.junior.customer.profilePicture']
: [];
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
}
updateAllowanceChangeRequestStatus(requestId: string, status: AllowanceChangeRequestStatus) {
return this.allowanceChangeRequestsRepository.update({ id: requestId }, { status });
}
findAllowanceChangeRequests(guardianId: string, query: PageOptionsRequestDto) {
return this.allowanceChangeRequestsRepository.findAndCount({
where: { allowance: { guardianId } },
take: query.size,
skip: query.size * (query.page - ONE),
relations: [
'allowance',
'allowance.junior',
'allowance.junior.customer',
'allowance.junior.customer.profilePicture',
],
});
}
}

View File

@ -1,47 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PageOptionsRequestDto } from '~/core/dtos';
import { CreateAllowanceRequestDto } from '../dtos/request';
import { Allowance } from '../entities';
const ONE = 1;
@Injectable()
export class AllowancesRepository {
constructor(@InjectRepository(Allowance) private readonly allowancesRepository: Repository<Allowance>) {}
createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
return this.allowancesRepository.save(
this.allowancesRepository.create({
guardianId,
name: body.name,
amount: body.amount,
frequency: body.frequency,
type: body.type,
startDate: body.startDate,
endDate: body.endDate,
numberOfTransactions: body.numberOfTransactions,
juniorId: body.juniorId,
}),
);
}
findAllowanceById(allowanceId: string, guardianId?: string) {
return this.allowancesRepository.findOne({
where: { id: allowanceId, guardianId },
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
});
}
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
return this.allowancesRepository.findAndCount({
where: { guardianId },
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
take: query.size,
skip: query.size * (query.page - ONE),
});
}
deleteAllowance(guardianId: string, allowanceId: string) {
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
}
}

View File

@ -1,2 +0,0 @@
export * from './allowance-change-request.repository';
export * from './allowances.repository';

View File

@ -1,115 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { PageOptionsRequestDto } from '~/core/dtos';
import { OciService } from '~/document/services';
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
import { AllowanceChangeRequest } from '../entities';
import { AllowanceChangeRequestStatus } from '../enums';
import { AllowanceChangeRequestsRepository } from '../repositories';
import { AllowancesService } from './allowances.service';
@Injectable()
export class AllowanceChangeRequestsService {
constructor(
private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository,
private readonly ociService: OciService,
private readonly allowanceService: AllowancesService,
) {}
async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) {
const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId);
if (allowance.amount === body.amount) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT');
}
const requestWithTheSameAmount = await this.findAllowanceChangeRequestBy({
allowanceId: body.allowanceId,
amount: body.amount,
status: AllowanceChangeRequestStatus.PENDING,
});
if (requestWithTheSameAmount) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING');
}
return this.allowanceChangeRequestsRepository.createAllowanceChangeRequest(body.allowanceId, body);
}
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>) {
return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where);
}
async approveAllowanceChangeRequest(guardianId: string, requestId: string) {
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
if (!request) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
}
if (request.status === AllowanceChangeRequestStatus.APPROVED) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED');
}
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
requestId,
AllowanceChangeRequestStatus.APPROVED,
);
}
async rejectAllowanceChangeRequest(guardianId: string, requestId: string) {
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
if (!request) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
}
if (request.status === AllowanceChangeRequestStatus.REJECTED) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_REJECTED');
}
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
requestId,
AllowanceChangeRequestStatus.REJECTED,
);
}
async findAllowanceChangeRequests(
guardianId: string,
query: PageOptionsRequestDto,
): Promise<[AllowanceChangeRequest[], number]> {
const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests(
guardianId,
query,
);
await this.prepareAllowanceChangeRequestsImages(requests);
return [requests, itemCount];
}
async findAllowanceChangeRequestById(guardianId: string, requestId: string) {
const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(
{
id: requestId,
allowance: { guardianId },
},
true,
);
if (!request) {
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
}
await this.prepareAllowanceChangeRequestsImages([request]);
return request;
}
private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) {
return Promise.all(
requests.map(async (request) => {
const profilePicture = request.allowance.junior.customer.profilePicture;
if (profilePicture) {
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
}
}),
);
}
}

View File

@ -1,85 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import moment from 'moment';
import { PageOptionsRequestDto } from '~/core/dtos';
import { OciService } from '~/document/services';
import { JuniorService } from '~/junior/services';
import { CreateAllowanceRequestDto } from '../dtos/request';
import { Allowance } from '../entities';
import { AllowancesRepository } from '../repositories';
@Injectable()
export class AllowancesService {
constructor(
private readonly allowancesRepository: AllowancesRepository,
private readonly juniorService: JuniorService,
private readonly ociService: OciService,
) {}
async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
if (moment(body.startDate).isBefore(moment().startOf('day'))) {
throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY');
}
if (moment(body.startDate).isAfter(body.endDate)) {
throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE');
}
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId);
if (!doesJuniorBelongToGuardian) {
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
}
const allowance = await this.allowancesRepository.createAllowance(guardianId, body);
return this.findAllowanceById(allowance.id);
}
async findAllowanceById(allowanceId: string, guardianId?: string) {
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId);
if (!allowance) {
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
}
await this.prepareAllowanceDocuments([allowance]);
return allowance;
}
async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> {
const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query);
await this.prepareAllowanceDocuments(allowances);
return [allowances, itemCount];
}
async deleteAllowance(guardianId: string, allowanceId: string) {
const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId);
if (!affected) {
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
}
}
async validateAllowanceForJunior(juniorId: string, allowanceId: string) {
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId);
if (!allowance) {
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
}
if (allowance.juniorId !== juniorId) {
throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR');
}
return allowance;
}
private async prepareAllowanceDocuments(allowance: Allowance[]) {
await Promise.all(
allowance.map(async (allowance) => {
const profilePicture = allowance.junior.customer.profilePicture;
if (profilePicture) {
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
}
}),
);
}
}

View File

@ -1,2 +0,0 @@
export * from './allowance-change-requests.service';
export * from './allowances.service';

View File

@ -1,31 +1,33 @@
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
import { LoggerModule } from 'nestjs-pino';
import { DataSource } from 'typeorm';
import { addTransactionalDataSource } from 'typeorm-transactional';
import { AllowanceModule } from './allowance/allowance.module';
import { AuthModule } from './auth/auth.module';
import { CardModule } from './card/card.module';
import { CacheModule } from './common/modules/cache/cache.module';
import { LookupModule } from './common/modules/lookup/lookup.module';
import { NeoLeapModule } from './common/modules/neoleap/neoleap.module';
import { NotificationModule } from './common/modules/notification/notification.module';
import { OtpModule } from './common/modules/otp/otp.module';
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
import { buildI18nOptions } from './core/module-options/i18n-options';
import { buildValidationPipe } from './core/pipes';
import { CronModule } from './cron/cron.module';
import { CustomerModule } from './customer/customer.module';
import { migrations } from './db';
import { DocumentModule } from './document/document.module';
import { GiftModule } from './gift/gift.module';
import { GuardianModule } from './guardian/guardian.module';
import { HealthModule } from './health/health.module';
import { JuniorModule } from './junior/junior.module';
import { MoneyRequestModule } from './money-request/money-request.module';
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
import { TaskModule } from './task/task.module';
import { UserModule } from './user/user.module';
import { WebhookModule } from './webhook/webhook.module';
@Module({
controllers: [],
@ -37,7 +39,6 @@ import { TaskModule } from './task/task.module';
useFactory: (config: ConfigService) => {
return buildTypeormOptions(config, migrations);
},
/* eslint-disable require-await */
async dataSourceFactory(options) {
if (!options) {
throw new Error('Invalid options passed');
@ -45,35 +46,34 @@ import { TaskModule } from './task/task.module';
return addTransactionalDataSource(new DataSource(options));
},
/* eslint-enable require-await */
}),
LoggerModule.forRootAsync({
useFactory: (config: ConfigService) => buildLoggerOptions(config),
inject: [ConfigService],
}),
I18nModule.forRoot(buildI18nOptions()),
CacheModule,
EventEmitterModule.forRoot(),
ScheduleModule.forRoot(),
// App modules
AuthModule,
UserModule,
CustomerModule,
JuniorModule,
TaskModule,
GuardianModule,
SavingGoalsModule,
AllowanceModule,
MoneyRequestModule,
GiftModule,
CardModule,
NotificationModule,
OtpModule,
DocumentModule,
LookupModule,
HealthModule,
NotificationModule,
CronModule,
NeoLeapModule,
WebhookModule,
],
providers: [
// Global Pipes

View File

@ -1,24 +1,16 @@
import { forwardRef, Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CustomerModule } from '~/customer/customer.module';
import { JuniorModule } from '~/junior/junior.module';
import { UserModule } from '~/user/user.module';
import { AuthController } from './controllers';
import { Device, User } from './entities';
import { DeviceRepository, UserRepository } from './repositories';
import { AuthService, DeviceService } from './services';
import { UserService } from './services/user.service';
import { AuthService } from './services';
import { AccessTokenStrategy } from './strategies';
@Module({
imports: [
TypeOrmModule.forFeature([User, Device]),
JwtModule.register({}),
forwardRef(() => CustomerModule),
forwardRef(() => JuniorModule),
],
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
imports: [JwtModule.register({}), UserModule, JuniorModule, HttpModule],
providers: [AuthService, AccessTokenStrategy],
controllers: [AuthController],
exports: [UserService, DeviceService],
exports: [],
})
export class AuthModule {}

View File

@ -1,31 +1,30 @@
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { DEVICE_ID_HEADER } from '~/common/constants';
import { AuthenticatedUser, Public } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import {
ChangePasswordRequestDto,
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
LoginRequestDto,
RefreshTokenRequestDto,
SendForgetPasswordOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
SetPasscodeRequestDto,
VerifyForgetPasswordOtpRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
import { LoginResponseDto } from '../dtos/response/login.response.dto';
import { VerifyForgetPasswordOtpResponseDto } from '../dtos/response/verify-forget-password-otp.response.dto';
import { IJwtPayload } from '../interfaces';
import { AuthService } from '../services';
@Controller('auth')
@ApiTags('Auth')
@ApiBearerAuth()
@ApiLangRequestHeader()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register/otp')
@ -40,51 +39,38 @@ export class AuthController {
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('register/set-email')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setEmail(@AuthenticatedUser() { sub }: IJwtPayload, @Body() setEmailDto: SetEmailRequestDto) {
await this.authService.setEmail(sub, setEmailDto);
}
@Post('register/set-passcode')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setPasscode(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { passcode }: SetPasscodeRequestDto) {
await this.authService.setPasscode(sub, passcode);
}
@Post('biometric/enable')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
return this.authService.enableBiometric(sub, enableBiometricDto);
}
@Post('biometric/disable')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
return this.authService.disableBiometric(sub, disableBiometricDto);
@Post('login')
async login(@Body() verifyUserDto: LoginRequestDto) {
const [res, user] = await this.authService.loginWithPassword(verifyUserDto);
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('forget-password/otp')
async forgetPassword(@Body() sendForgetPasswordOtpDto: SendForgetPasswordOtpRequestDto) {
const email = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(email));
const maskedNumber = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(maskedNumber));
}
@Post('forget-password/verify')
@HttpCode(HttpStatus.OK)
@ApiDataResponse(VerifyForgetPasswordOtpResponseDto)
async verifyForgetPasswordOtp(@Body() forgetPasswordDto: VerifyForgetPasswordOtpRequestDto) {
const { token, user } = await this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
return ResponseFactory.data(new VerifyForgetPasswordOtpResponseDto(token, user));
}
@Post('forget-password/reset')
@HttpCode(HttpStatus.NO_CONTENT)
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
return this.authService.resetPassword(forgetPasswordDto);
}
@Post('junior/set-passcode')
@Post('change-password')
@HttpCode(HttpStatus.NO_CONTENT)
@Public()
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
return this.authService.setJuniorPasscode(setPasscodeDto);
@UseGuards(AccessTokenGuard)
async changePassword(@AuthenticatedUser() { sub }: IJwtPayload, @Body() forgetPasswordDto: ChangePasswordRequestDto) {
return this.authService.changePassword(sub, forgetPasswordDto);
}
@Post('refresh-token')
@ -94,12 +80,6 @@ export class AuthController {
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('login')
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
const [res, user] = await this.authService.login(loginDto, deviceId);
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)

View File

@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { PASSWORD_REGEX } from '~/auth/constants';
export class ChangePasswordRequestDto {
@ApiProperty({ example: 'currentPassword@123' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.currentPassword' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.currentPassword' }) })
currentPassword!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.newPassword' }),
})
newPassword!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmNewPassword' }),
})
confirmNewPassword!: string;
}

View File

@ -1,19 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
import { OmitType } from '@nestjs/swagger';
import { VerifyUserRequestDto } from './verify-user.request.dto';
export class CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode: string = '+966';
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
}
export class CreateUnverifiedUserRequestDto extends OmitType(VerifyUserRequestDto, ['otp']) {}

View File

@ -1,4 +0,0 @@
import { PickType } from '@nestjs/swagger';
import { EnableBiometricRequestDto } from './enable-biometric.request.dto';
export class DisableBiometricRequestDto extends PickType(EnableBiometricRequestDto, ['deviceId']) {}

View File

@ -1,14 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class EnableBiometricRequestDto {
@ApiProperty({ example: 'device-id' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceId' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.deviceId' }) })
deviceId!: string;
@ApiProperty({ example: 'publicKey' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.publicKey' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.publicKey' }) })
publicKey!: string;
}

View File

@ -1,32 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator';
import { IsString, Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class ForgetPasswordRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: 'password' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.password' }),
})
password!: string;
@ApiProperty({ example: 'password' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.confirmPassword' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.confirmPassword' }) })
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmPassword' }),
})
confirmPassword!: string;
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
)
@MaxLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
@MinLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
otp!: string;
@ApiProperty({ example: 'reset-token-32423123' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.resetPasswordToken' }) })
resetPasswordToken!: string;
}

View File

@ -1,11 +1,11 @@
export * from './change-password.request.dto';
export * from './create-unverified-user.request.dto';
export * from './disable-biometric.request.dto';
export * from './enable-biometric.request.dto';
export * from './forget-password.request.dto';
export * from './login.request.dto';
export * from './refresh-token.request.dto';
export * from './send-forget-password-otp.request.dto';
export * from './set-email.request.dto';
export * from './set-junior-password.request.dto';
export * from './set-passcode.request.dto';
export * from './verify-forget-password-otp.request.dto';
export * from './verify-otp.request.dto';
export * from './verify-user.request.dto';

View File

@ -1,30 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, Matches, ValidateIf } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { GrantType } from '~/auth/enums';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class LoginRequestDto {
@ApiProperty({ example: GrantType.PASSWORD })
@IsEnum(GrantType, { message: i18n('validation.IsEnum', { path: 'general', property: 'auth.grantType' }) })
grantType!: GrantType;
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: 'test@test.com' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: '123456' })
@ApiProperty({ example: 'Abcd1234@' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
password!: string;
@ApiProperty({ example: 'fcm-device-token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.fcmToken' }) })
@IsOptional()
fcmToken?: string;
@ApiProperty({ example: 'Login signature' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.signature' }) })
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
signature!: string;
}

View File

@ -1,4 +1,4 @@
import { PickType } from '@nestjs/swagger';
import { LoginRequestDto } from './login.request.dto';
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['email']) {}
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['countryCode', 'phoneNumber']) {}

View File

@ -0,0 +1,23 @@
import { ApiProperty, PickType } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { ForgetPasswordRequestDto } from './forget-password.request.dto';
export class VerifyForgetPasswordOtpRequestDto extends PickType(ForgetPasswordRequestDto, [
'countryCode',
'phoneNumber',
]) {
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
)
@MaxLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
@MinLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
otp!: string;
}

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
export class VerifyOtpRequestDto {
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
)
@MaxLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
@MinLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
otp!: string;
}

View File

@ -1,10 +1,73 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import {
IsDateString,
IsEmail,
IsEnum,
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
import { CountryIso } from '~/common/enums';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
import { IsAbove18, IsValidPhoneNumber } from '~/core/decorators/validations';
export class VerifyUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
firstName!: string;
@ApiProperty({ example: 'Doe' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
lastName!: string;
@ApiProperty({ example: '2001-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
dateOfBirth!: Date;
@ApiProperty({ example: 'JO' })
@IsEnum(CountryIso, {
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.countryOfResidence' }),
})
@IsOptional()
countryOfResidence: CountryIso = CountryIso.SAUDI_ARABIA;
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
@IsOptional()
email!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.password' }),
})
password!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmPassword' }),
})
confirmPassword!: string;
export class VerifyUserRequestDto extends CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { ILoginResponse } from '~/auth/interfaces';
import { CustomerResponseDto } from '~/customer/dtos/response';
import { User } from '~/user/entities';
import { UserResponseDto } from './user.response.dto';
export class LoginResponseDto {
@ -17,12 +17,12 @@ export class LoginResponseDto {
@ApiProperty({ example: UserResponseDto })
user!: UserResponseDto;
@ApiProperty({ example: CustomerResponseDto })
customer!: CustomerResponseDto;
@ApiProperty({ type: CustomerResponseDto })
customer!: CustomerResponseDto | null;
constructor(IVerifyUserResponse: ILoginResponse, user: User) {
this.user = new UserResponseDto(user);
this.customer = new CustomerResponseDto(user.customer);
this.customer = user.customer ? new CustomerResponseDto(user.customer) : null;
this.accessToken = IVerifyUserResponse.accessToken;
this.refreshToken = IVerifyUserResponse.refreshToken;
this.expiresAt = IVerifyUserResponse.expiresAt;

View File

@ -1,7 +1,7 @@
export class SendForgetPasswordOtpResponseDto {
email!: string;
maskedNumber!: string;
constructor(email: string) {
this.email = email;
constructor(maskedNumber: string) {
this.maskedNumber = maskedNumber;
}
}

View File

@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpResponseDto {
@ApiProperty()
phoneNumber!: string;
maskedNumber!: string;
constructor(phoneNumber: string) {
this.phoneNumber = phoneNumber;
constructor(maskedNumber: string) {
this.maskedNumber = maskedNumber;
}
}

View File

@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpV2ResponseDto {
@ApiProperty()
maskedNumber!: string;
constructor(maskedNumber: string) {
this.maskedNumber = maskedNumber;
}
}

View File

@ -1,36 +1,48 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { Roles } from '~/auth/enums';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { User } from '~/user/entities';
export class UserResponseDto {
@ApiProperty()
id!: string;
@ApiProperty()
email!: string;
countryCode!: string;
@ApiProperty()
phoneNumber!: string;
@ApiProperty()
countryCode!: string;
email!: string;
@ApiProperty()
isPasswordSet!: boolean;
firstName!: string;
@ApiProperty()
isProfileCompleted!: boolean;
lastName!: string;
@ApiProperty()
roles!: Roles[];
dateOfBirth!: Date;
@ApiPropertyOptional({ type: DocumentMetaResponseDto, nullable: true })
profilePicture!: DocumentMetaResponseDto | null;
@ApiProperty()
isPhoneVerified!: boolean;
@ApiProperty()
isEmailVerified!: boolean;
constructor(user: User) {
this.id = user.id;
this.email = user.email;
this.phoneNumber = user.phoneNumber;
this.countryCode = user.countryCode;
this.isPasswordSet = user.isPasswordSet;
this.isProfileCompleted = user.isProfileCompleted;
this.roles = user.roles;
this.phoneNumber = user.phoneNumber;
this.dateOfBirth = user.customer?.dateOfBirth;
this.email = user.email;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.profilePicture = user.profilePicture ? new DocumentMetaResponseDto(user.profilePicture) : null;
this.isEmailVerified = user.isEmailVerified;
this.isPhoneVerified = user.isPhoneVerified;
}
}

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/user/entities';
export class VerifyForgetPasswordOtpResponseDto {
@ApiProperty()
phoneNumber!: string;
@ApiProperty()
countryCode!: string;
@ApiProperty()
resetPasswordToken!: string;
constructor(token: string, user: User) {
this.phoneNumber = user.phoneNumber;
this.countryCode = user.countryCode;
this.resetPasswordToken = token;
}
}

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { ILoginResponse } from '~/auth/interfaces';
import { User } from '~/user/entities';
export class VerifyUserResponseDto {
@ApiProperty()

View File

@ -1,4 +1,6 @@
export enum Roles {
JUNIOR = 'JUNIOR',
GUARDIAN = 'GUARDIAN',
CHECKER = 'CHECKER',
SUPER_ADMIN = 'SUPER_ADMIN',
}

View File

@ -1,60 +1,78 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { Request } from 'express';
import moment from 'moment';
import { CacheService } from '~/common/modules/cache/services';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
import { JuniorTokenService } from '~/junior/services';
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
import { UserType } from '~/user/enums';
import { DeviceService, UserService, UserTokenService } from '~/user/services';
import { User } from '../../user/entities';
import {
ChangePasswordRequestDto,
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
LoginRequestDto,
SendForgetPasswordOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
VerifyForgetPasswordOtpRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
import { User } from '../entities';
import { GrantType, Roles } from '../enums';
import { Roles } from '../enums';
import { IJwtPayload, ILoginResponse } from '../interfaces';
import { removePadding, verifySignature } from '../utils';
import { DeviceService } from './device.service';
import { UserService } from './user.service';
const ONE_THOUSAND = 1000;
const SALT_ROUNDS = 10;
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private readonly otpService: OtpService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly userService: UserService,
private readonly deviceService: DeviceService,
private readonly juniorTokenService: JuniorTokenService,
private readonly userTokenService: UserTokenService,
private readonly cacheService: CacheService,
) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
async sendRegisterOtp(body: CreateUnverifiedUserRequestDto) {
if (body.email) {
const isEmailUsed = await this.userService.findUser({ email: body.email, isEmailVerified: true });
if (isEmailUsed) {
this.logger.error(`Email ${body.email} is already used`);
throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN');
}
}
if (body.password !== body.confirmPassword) {
this.logger.error('Password and confirm password do not match');
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
this.logger.log(`Sending OTP to ${body.countryCode + body.phoneNumber}`);
const user = await this.userService.findOrCreateUser(body);
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.phoneNumber,
recipient: user.fullPhoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
});
}
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber });
this.logger.log(`Verifying user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`);
const user = await this.userService.findUserOrThrow({
phoneNumber: verifyUserDto.phoneNumber,
countryCode: verifyUserDto.countryCode,
});
if (user.isPasswordSet) {
throw new BadRequestException('USERS.PHONE_ALREADY_VERIFIED');
if (user.isPhoneVerified) {
this.logger.error(`User with phone number ${user.fullPhoneNumber} already verified`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_VERIFIED');
}
const isOtpValid = await this.otpService.verifyOtp({
@ -65,208 +83,203 @@ export class AuthService {
});
if (!isOtpValid) {
throw new BadRequestException('USERS.INVALID_OTP');
this.logger.error(`Invalid OTP for user with phone number ${user.fullPhoneNumber}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
const updatedUser = await this.userService.verifyUserAndCreateCustomer(user);
await this.userService.verifyUser(user.id, verifyUserDto);
const tokens = await this.generateAuthToken(updatedUser);
await user.reload();
return [tokens, updatedUser];
const tokens = await this.generateAuthToken(user);
this.logger.log(`User with phone number ${user.fullPhoneNumber} verified successfully`);
return [tokens, user];
}
async setEmail(userId: string, { email }: SetEmailRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.email) {
throw new BadRequestException('USERS.EMAIL_ALREADY_SET');
}
const existingUser = await this.userService.findUser({ email });
if (existingUser) {
throw new BadRequestException('USERS.EMAIL_ALREADY_TAKEN');
}
return this.userService.setEmail(userId, email);
}
async setPasscode(userId: string, passcode: string) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.password) {
throw new BadRequestException('USERS.PASSCODE_ALREADY_SET');
}
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(passcode, salt);
await this.userService.setPasscode(userId, hashedPasscode, salt);
}
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
return this.deviceService.createDevice({
deviceId,
userId,
publicKey,
});
}
if (device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey });
}
async disableBiometric(userId: string, { deviceId }: DisableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
throw new BadRequestException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) {
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
}
async sendForgetPasswordOtp({ countryCode, phoneNumber }: SendForgetPasswordOtpRequestDto) {
this.logger.log(`Sending forget password OTP to ${countryCode + phoneNumber}`);
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.email,
recipient: user.fullPhoneNumber,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
otpType: OtpType.SMS,
});
}
async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) {
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
}
async verifyForgetPasswordOtp({ countryCode, phoneNumber, otp }: VerifyForgetPasswordOtpRequestDto) {
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
const isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
otpType: OtpType.SMS,
value: otp,
});
if (!isOtpValid) {
throw new BadRequestException('USERS.INVALID_OTP');
this.logger.error(`Invalid OTP for user with phone number ${user.fullPhoneNumber}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
this.validatePassword(password, confirmPassword, user);
// generate a token for the user to reset password
const token = await this.userTokenService.generateToken(user.id, moment().add(5, 'minutes').toDate());
return { token, user };
}
async resetPassword({
countryCode,
phoneNumber,
resetPasswordToken,
password,
confirmPassword,
}: ForgetPasswordRequestDto) {
this.logger.log(`Verifying forget password OTP for ${countryCode + phoneNumber}`);
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
await this.userTokenService.validateToken(
resetPasswordToken,
user.roles.includes(Roles.GUARDIAN) ? UserType.GUARDIAN : UserType.JUNIOR,
);
if (password !== confirmPassword) {
this.logger.error('Password and confirm password do not match');
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
const isOldPassword = bcrypt.compareSync(password, user.password);
if (isOldPassword) {
this.logger.error(
`New password cannot be the same as the current password for user with phone number ${user.fullPhoneNumber}`,
);
throw new BadRequestException('AUTH.PASSWORD_SAME_AS_CURRENT');
}
const hashedPassword = bcrypt.hashSync(password, user.salt);
await this.userService.setPasscode(user.id, hashedPassword, user.salt);
await this.userService.setPassword(user.id, hashedPassword, user.salt);
await this.userTokenService.invalidateToken(resetPasswordToken);
this.logger.log(`Passcode updated successfully for user with phone number ${user.fullPhoneNumber}`);
}
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({ email: loginDto.email });
let tokens;
async changePassword(userId: string, { currentPassword, newPassword, confirmNewPassword }: ChangePasswordRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (!user) {
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
if (!user.isPasswordSet) {
this.logger.error(`Password not set for user with id ${userId}`);
throw new BadRequestException('AUTH.PASSWORD_NOT_SET');
}
if (loginDto.grantType === GrantType.PASSWORD) {
tokens = await this.loginWithPassword(loginDto, user);
} else {
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
if (currentPassword === newPassword) {
this.logger.error('New password cannot be the same as current password');
throw new BadRequestException('AUTH.PASSWORD_SAME_AS_CURRENT');
}
this.deviceService.updateDevice(deviceId, {
lastAccessOn: new Date(),
fcmToken: loginDto.fcmToken,
userId: user.id,
});
if (newPassword !== confirmNewPassword) {
this.logger.error('New password and confirm new password do not match');
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
return [tokens, user];
this.logger.log(`Validating current password for user with id ${userId}`);
const isCurrentPasswordValid = bcrypt.compareSync(currentPassword, user.password);
if (!isCurrentPasswordValid) {
this.logger.error(`Invalid current password for user with id ${userId}`);
throw new UnauthorizedException('AUTH.INVALID_CURRENT_PASSWORD');
}
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedNewPassword = bcrypt.hashSync(newPassword, salt);
await this.userService.setPassword(user.id, hashedNewPassword, salt);
this.logger.log(`Password changed successfully for user with id ${userId}`);
}
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`);
const juniorId = await this.userTokenService.validateToken(body.qrToken, UserType.JUNIOR);
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
await this.juniorTokenService.invalidateToken(body.qrToken);
await this.userService.setPassword(juniorId!, hashedPasscode, salt);
await this.userTokenService.invalidateToken(body.qrToken);
this.logger.log(`Passcode set successfully for junior with id ${juniorId}`);
}
async refreshToken(refreshToken: string): Promise<[ILoginResponse, User]> {
this.logger.log('Refreshing token');
const isBlackListed = await this.cacheService.get(refreshToken);
if (isBlackListed) {
this.logger.error('Refresh token is blacklisted');
throw new BadRequestException('AUTH.INVALID_REFRESH_TOKEN');
}
try {
const isValid = await this.jwtService.verifyAsync<IJwtPayload>(refreshToken, {
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
});
this.logger.log(`Refreshing token for user with id ${isValid.sub}`);
const user = await this.userService.findUserOrThrow({ id: isValid.sub });
const tokens = await this.generateAuthToken(user);
this.logger.log(`Blacklisting old tokens for user with id ${isValid.sub}`);
const refreshTokenExpiry = this.jwtService.decode(refreshToken).exp - Date.now() / ONE_THOUSAND;
await this.cacheService.set(refreshToken, 'BLACKLISTED', refreshTokenExpiry);
this.logger.log(`Token refreshed successfully for user with id ${isValid.sub}`);
return [tokens, user];
} catch (error) {
this.logger.error('Invalid refresh token');
throw new BadRequestException('AUTH.INVALID_REFRESH_TOKEN');
}
}
logout(req: Request) {
this.logger.log('Logging out');
const accessToken = req.headers.authorization?.split(' ')[1] as string;
const expiryInTtl = this.jwtService.decode(accessToken).exp - Date.now() / ONE_THOUSAND;
return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl);
return this.cacheService.set(accessToken, 'BLACKLISTED', expiryInTtl);
}
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
async loginWithPassword(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({
countryCode: loginDto.countryCode,
phoneNumber: loginDto.phoneNumber,
});
if (!user) {
this.logger.error(`User not found with phone number ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
if (!user.password) {
this.logger.error(`Password not set for user with phone number ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.PHONE_NUMBER_NOT_VERIFIED');
}
this.logger.log(`validating password for user with phone ${loginDto.countryCode + loginDto.phoneNumber}`);
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
if (!isPasswordValid) {
this.logger.error(`Invalid password for user with phone ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
const tokens = await this.generateAuthToken(user);
return tokens;
}
private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise<ILoginResponse> {
const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
if (!device) {
throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
}
const cleanToken = removePadding(loginDto.signature);
const isValidToken = await verifySignature(
device.publicKey,
cleanToken,
`${user.email} - ${device.deviceId}`,
'SHA1',
);
if (!isValidToken) {
throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
}
const tokens = await this.generateAuthToken(user);
return tokens;
this.logger.log(`Password validated successfully for user`);
return [tokens, user];
}
private async generateAuthToken(user: User) {
this.logger.log(`Generating auth token for user with id ${user.id}`);
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.sign(
{ sub: user.id, roles: user.roles },
@ -284,22 +297,7 @@ export class AuthService {
),
]);
this.logger.log(`Auth token generated successfully for user with id ${user.id}`);
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
}
private validatePassword(password: string, confirmPassword: string, user: User) {
if (password !== confirmPassword) {
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
const roles = user.roles;
if (roles.includes(Roles.GUARDIAN) && !PASSCODE_REGEX.test(password)) {
throw new BadRequestException('AUTH.INVALID_PASSCODE');
}
if (roles.includes(Roles.JUNIOR) && !PASSWORD_REGEX.test(password)) {
throw new BadRequestException('AUTH.INVALID_PASSWORD');
}
}
}

View File

@ -1,3 +1 @@
export * from './auth.service';
export * from './device.service';
export * from './user.service';

View File

@ -1,75 +0,0 @@
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
import { CustomerService } from '~/customer/services';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
import { User } from '../entities';
import { Roles } from '../enums';
import { UserRepository } from '../repositories';
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
) {}
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
return this.userRepository.findOne(where);
}
async findUserOrThrow(where: FindOptionsWhere<User>) {
const user = await this.findUser(where);
if (!user) {
throw new BadRequestException('USERS.NOT_FOUND');
}
return user;
}
async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userRepository.findOne({ phoneNumber });
if (!user) {
return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] });
}
if (user && user.roles.includes(Roles.GUARDIAN) && user.isPasswordSet) {
throw new BadRequestException('USERS.PHONE_NUMBER_ALREADY_EXISTS');
}
if (user && user.roles.includes(Roles.JUNIOR)) {
throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
//TODO add role Guardian to the existing user and send OTP
}
return user;
}
async createUser(data: Partial<User>) {
const user = await this.userRepository.createUser(data);
return user;
}
setEmail(userId: string, email: string) {
return this.userRepository.update(userId, { email });
}
setPasscode(userId: string, passcode: string, salt: string) {
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
}
async verifyUserAndCreateCustomer(user: User) {
await this.customerService.createCustomer(
{
guardian: Guardian.create({ id: user.id }),
notificationSettings: new CustomerNotificationSettings(),
},
user,
);
return this.findUserOrThrow({ id: user.id });
}
}

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '~/user/services';
import { IJwtPayload } from '../interfaces';
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'access-token') {
constructor(configService: ConfigService) {
constructor(configService: ConfigService, private userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
@ -14,7 +15,13 @@ export class AccessTokenStrategy extends PassportStrategy(Strategy, 'access-toke
});
}
validate(payload: IJwtPayload) {
async validate(payload: IJwtPayload) {
const user = await this.userService.findUser({ id: payload.sub });
if (!user) {
throw new UnauthorizedException();
}
return payload;
}
}

33
src/card/card.module.ts Normal file
View File

@ -0,0 +1,33 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NeoLeapModule } from '~/common/modules/neoleap/neoleap.module';
import { CustomerModule } from '~/customer/customer.module';
import { CardsController } from './controllers';
import { Card } from './entities';
import { Account } from './entities/account.entity';
import { Transaction } from './entities/transaction.entity';
import { CardRepository } from './repositories';
import { AccountRepository } from './repositories/account.repository';
import { TransactionRepository } from './repositories/transaction.repository';
import { CardService } from './services';
import { AccountService } from './services/account.service';
import { TransactionService } from './services/transaction.service';
@Module({
imports: [
TypeOrmModule.forFeature([Card, Account, Transaction]),
forwardRef(() => NeoLeapModule),
forwardRef(() => CustomerModule), // <-- add forwardRef here
],
providers: [
CardService,
CardRepository,
TransactionService,
TransactionRepository,
AccountService,
AccountRepository,
],
exports: [CardService, TransactionService],
controllers: [CardsController],
})
export class CardModule {}

View File

@ -0,0 +1,39 @@
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { IJwtPayload } from '~/auth/interfaces';
import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { CardEmbossingDetailsResponseDto } from '~/common/modules/neoleap/dtos/response';
import { ApiDataResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import { CardResponseDto } from '../dtos/responses';
import { CardService } from '../services';
@Controller('cards')
@ApiBearerAuth()
@ApiTags('Cards')
@UseGuards(AccessTokenGuard)
export class CardsController {
constructor(private readonly cardService: CardService) {}
@Post()
@ApiDataResponse(CardResponseDto)
async createCard(@AuthenticatedUser() { sub }: IJwtPayload) {
const card = await this.cardService.createCard(sub);
return ResponseFactory.data(new CardResponseDto(card));
}
@Get('current')
@ApiDataResponse(CardResponseDto)
async getCurrentCard(@AuthenticatedUser() { sub }: IJwtPayload) {
const card = await this.cardService.getCardByCustomerId(sub);
return ResponseFactory.data(new CardResponseDto(card));
}
@Get('embossing-details')
@ApiDataResponse(CardEmbossingDetailsResponseDto)
async getCardById(@AuthenticatedUser() { sub }: IJwtPayload) {
const res = await this.cardService.getEmbossingInformation(sub);
return ResponseFactory.data(res);
}
}

View File

@ -0,0 +1 @@
export * from './cards.controller';

View File

@ -0,0 +1,56 @@
import { ApiProperty } from '@nestjs/swagger';
import { Card } from '~/card/entities';
import { CardScheme, CardStatus, CustomerType } from '~/card/enums';
import { CardStatusDescriptionMapper } from '~/card/mappers/card-status-description.mapper';
import { UserLocale } from '~/core/enums';
export class CardResponseDto {
@ApiProperty({
example: 'b34df8c2-5d3e-4b1a-9c2f-7e3b1a2d3f4e',
})
id!: string;
@ApiProperty({
example: '123456',
description: 'The first six digits of the card number.',
})
firstSixDigits!: string;
@ApiProperty({ example: '7890', description: 'The last four digits of the card number.' })
lastFourDigits!: string;
@ApiProperty({
enum: CardScheme,
description: 'The card scheme (e.g., VISA, MASTERCARD).',
})
scheme!: CardScheme;
@ApiProperty({
enum: CardStatus,
description: 'The current status of the card (e.g., ACTIVE, PENDING).',
})
status!: CardStatus;
@ApiProperty({
example: 'The card is active',
description: 'A description of the card status.',
})
statusDescription!: string;
@ApiProperty({
example: 2000.0,
description: 'The credit limit of the card.',
})
balance!: number;
constructor(card: Card) {
this.id = card.id;
this.firstSixDigits = card.firstSixDigits;
this.lastFourDigits = card.lastFourDigits;
this.scheme = card.scheme;
this.status = card.status;
this.statusDescription = CardStatusDescriptionMapper[card.statusDescription][UserLocale.ENGLISH].description;
this.balance =
card.customerType === CustomerType.CHILD ? Math.min(card.limit, card.account.balance) : card.account.balance;
}
}

View File

@ -0,0 +1 @@
export * from './card.response.dto';

View File

@ -0,0 +1,39 @@
import { Column, CreateDateColumn, Entity, Index, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Card } from './card.entity';
import { Transaction } from './transaction.entity';
@Entity('accounts')
export class Account {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column('varchar', { length: 255, nullable: false, unique: true, name: 'account_reference' })
@Index({ unique: true })
accountReference!: string;
@Index({ unique: true })
@Column('varchar', { length: 255, nullable: false, name: 'account_number' })
accountNumber!: string;
@Index({ unique: true })
@Column('varchar', { length: 255, nullable: false, name: 'iban' })
iban!: string;
@Column('varchar', { length: 255, nullable: false, name: 'currency' })
currency!: string;
@Column('decimal', { precision: 10, scale: 2, default: 0.0, name: 'balance' })
balance!: number;
@OneToMany(() => Card, (card) => card.account, { cascade: true })
cards!: Card[];
@OneToMany(() => Transaction, (transaction) => transaction.account, { cascade: true })
transactions!: Transaction[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
updatedAt!: Date;
}

View File

@ -0,0 +1,89 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Customer } from '~/customer/entities';
import { CardColors, CardIssuers, CardScheme, CardStatus, CardStatusDescription, CustomerType } from '../enums';
import { Account } from './account.entity';
import { Transaction } from './transaction.entity';
@Entity('cards')
export class Card {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Index({ unique: true })
@Column({ name: 'card_reference', nullable: false, type: 'varchar' })
cardReference!: string;
@Index({ unique: true })
@Column({ name: 'vpan', nullable: false, type: 'varchar' })
vpan!: string;
@Column({ length: 6, name: 'first_six_digits', nullable: false, type: 'varchar' })
firstSixDigits!: string;
@Column({ length: 4, name: 'last_four_digits', nullable: false, type: 'varchar' })
lastFourDigits!: string;
@Column({ type: 'varchar', nullable: false })
expiry!: string;
@Column({ type: 'varchar', nullable: false, name: 'customer_type' })
customerType!: CustomerType;
@Column({ type: 'varchar', nullable: false, default: CardColors.BLUE })
color!: CardColors;
@Column({ type: 'varchar', nullable: false, default: CardStatus.PENDING })
status!: CardStatus;
@Column({ type: 'varchar', nullable: false, default: CardStatusDescription.PENDING_ACTIVATION })
statusDescription!: CardStatusDescription;
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0.0, name: 'limit' })
limit!: number;
@Column({ type: 'varchar', nullable: false, default: CardScheme.VISA })
scheme!: CardScheme;
@Column({ type: 'varchar', nullable: false })
issuer!: CardIssuers;
@Column({ type: 'uuid', name: 'customer_id', nullable: false })
customerId!: string;
@Column({ type: 'uuid', name: 'parent_id', nullable: true })
parentId?: string;
@Column({ type: 'uuid', name: 'account_id', nullable: false })
accountId!: string;
@ManyToOne(() => Customer, (customer) => customer.childCards)
@JoinColumn({ name: 'parent_id' })
parentCustomer?: Customer;
@ManyToOne(() => Customer, (customer) => customer.cards, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'customer_id' })
customer!: Customer;
@ManyToOne(() => Account, (account) => account.cards, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account!: Account;
@OneToMany(() => Transaction, (transaction) => transaction.card, { cascade: true })
transactions!: Transaction[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamp with time zone', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -0,0 +1 @@
export * from './card.entity';

View File

@ -0,0 +1,69 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { TransactionScope, TransactionType } from '../enums';
import { Account } from './account.entity';
import { Card } from './card.entity';
@Entity('transactions')
export class Transaction {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'transaction_scope', type: 'varchar', nullable: false })
transactionScope!: TransactionScope;
@Column({ name: 'transaction_type', type: 'varchar', default: TransactionType.EXTERNAL })
transactionType!: TransactionType;
@Column({ name: 'card_reference', nullable: true, type: 'varchar' })
cardReference!: string;
@Column({ name: 'account_reference', nullable: true, type: 'varchar' })
accountReference!: string;
@Column({ name: 'transaction_id', unique: true, nullable: true, type: 'varchar' })
transactionId!: string;
@Column({ name: 'card_masked_number', nullable: true, type: 'varchar' })
cardMaskedNumber!: string;
@Column({ type: 'timestamp with time zone', name: 'transaction_date', nullable: true })
transactionDate!: Date;
@Column({ name: 'rrn', nullable: true, type: 'varchar' })
rrn!: string;
@Column({ type: 'decimal', precision: 12, scale: 2, name: 'transaction_amount' })
transactionAmount!: number;
@Column({ type: 'varchar', name: 'transaction_currency' })
transactionCurrency!: string;
@Column({ type: 'decimal', name: 'billing_amount', precision: 12, scale: 2 })
billingAmount!: number;
@Column({ type: 'decimal', name: 'settlement_amount', precision: 12, scale: 2 })
settlementAmount!: number;
@Column({ type: 'decimal', name: 'fees', precision: 12, scale: 2 })
fees!: number;
@Column({ type: 'decimal', name: 'vat_on_fees', precision: 12, scale: 2, default: 0.0 })
vatOnFees!: number;
@Column({ name: 'card_id', type: 'uuid', nullable: true })
cardId!: string;
@Column({ name: 'account_id', type: 'uuid', nullable: true })
accountId!: string;
@ManyToOne(() => Card, (card) => card.transactions, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'card_id' })
card!: Card;
@ManyToOne(() => Account, (account) => account.transactions, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'account_id' })
account!: Account;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
}

View File

@ -0,0 +1,4 @@
export enum CardColors {
RED = 'RED',
BLUE = 'BLUE',
}

View File

@ -0,0 +1,3 @@
export enum CardIssuers {
NEOLEAP = 'NEOLEAP',
}

View File

@ -0,0 +1,4 @@
export enum CardScheme {
VISA = 'VISA',
MASTERCARD = 'MASTERCARD',
}

View File

@ -0,0 +1,68 @@
/**
* import { CardStatus, CardStatusDescription } from '../enums';
export const CardStatusMapper: Record<string, { description: CardStatusDescription; status: CardStatus }> = {
//ACTIVE
'00': { description: 'NORMAL', status: CardStatus.ACTIVE },
//PENDING
'02': { description: 'NOT_YET_ISSUED', status: CardStatus.PENDING },
'20': { description: 'PENDING_ISSUANCE', status: CardStatus.PENDING },
'21': { description: 'CARD_EXTRACTED', status: CardStatus.PENDING },
'22': { description: 'EXTRACTION_FAILED', status: CardStatus.PENDING },
'23': { description: 'FAILED_PRINTING_BULK', status: CardStatus.PENDING },
'24': { description: 'FAILED_PRINTING_INST', status: CardStatus.PENDING },
'30': { description: 'PENDING_ACTIVATION', status: CardStatus.PENDING },
'27': { description: 'PENDING_PIN', status: CardStatus.PENDING },
'16': { description: 'PREPARE_TO_CLOSE', status: CardStatus.PENDING },
//BLOCKED
'01': { description: 'PIN_TRIES_EXCEEDED', status: CardStatus.BLOCKED },
'03': { description: 'CARD_EXPIRED', status: CardStatus.BLOCKED },
'04': { description: 'LOST', status: CardStatus.BLOCKED },
'05': { description: 'STOLEN', status: CardStatus.BLOCKED },
'06': { description: 'CUSTOMER_CLOSE', status: CardStatus.BLOCKED },
'07': { description: 'BANK_CANCELLED', status: CardStatus.BLOCKED },
'08': { description: 'FRAUD', status: CardStatus.BLOCKED },
'09': { description: 'DAMAGED', status: CardStatus.BLOCKED },
'50': { description: 'SAFE_BLOCK', status: CardStatus.BLOCKED },
'51': { description: 'TEMPORARY_BLOCK', status: CardStatus.BLOCKED },
'52': { description: 'RISK_BLOCK', status: CardStatus.BLOCKED },
'53': { description: 'OVERDRAFT', status: CardStatus.BLOCKED },
'54': { description: 'BLOCKED_FOR_FEES', status: CardStatus.BLOCKED },
'67': { description: 'CLOSED_CUSTOMER_DEAD', status: CardStatus.BLOCKED },
'75': { description: 'RETURN_CARD', status: CardStatus.BLOCKED },
//Fallback
'99': { description: 'UNKNOWN', status: CardStatus.PENDING },
};
*/
export enum CardStatusDescription {
NORMAL = 'NORMAL',
NOT_YET_ISSUED = 'NOT_YET_ISSUED',
PENDING_ISSUANCE = 'PENDING_ISSUANCE',
CARD_EXTRACTED = 'CARD_EXTRACTED',
EXTRACTION_FAILED = 'EXTRACTION_FAILED',
FAILED_PRINTING_BULK = 'FAILED_PRINTING_BULK',
FAILED_PRINTING_INST = 'FAILED_PRINTING_INST',
PENDING_ACTIVATION = 'PENDING_ACTIVATION',
PENDING_PIN = 'PENDING_PIN',
PREPARE_TO_CLOSE = 'PREPARE_TO_CLOSE',
PIN_TRIES_EXCEEDED = 'PIN_TRIES_EXCEEDED',
CARD_EXPIRED = 'CARD_EXPIRED',
LOST = 'LOST',
STOLEN = 'STOLEN',
CUSTOMER_CLOSE = 'CUSTOMER_CLOSE',
BANK_CANCELLED = 'BANK_CANCELLED',
FRAUD = 'FRAUD',
DAMAGED = 'DAMAGED',
SAFE_BLOCK = 'SAFE_BLOCK',
TEMPORARY_BLOCK = 'TEMPORARY_BLOCK',
RISK_BLOCK = 'RISK_BLOCK',
OVERDRAFT = 'OVERDRAFT',
BLOCKED_FOR_FEES = 'BLOCKED_FOR_FEES',
CLOSED_CUSTOMER_DEAD = 'CLOSED_CUSTOMER_DEAD',
RETURN_CARD = 'RETURN_CARD',
UNKNOWN = 'UNKNOWN',
}

View File

@ -0,0 +1,6 @@
export enum CardStatus {
ACTIVE = 'ACTIVE',
CANCELED = 'CANCELED',
BLOCKED = 'BLOCKED',
PENDING = 'PENDING',
}

View File

@ -0,0 +1,4 @@
export enum CustomerType {
PARENT = 'PARENT',
CHILD = 'CHILD',
}

8
src/card/enums/index.ts Normal file
View File

@ -0,0 +1,8 @@
export * from './card-colors.enum';
export * from './card-issuers.enum';
export * from './card-scheme.enum';
export * from './card-status-description.enum';
export * from './card-status.enum';
export * from './customer-type.enum';
export * from './transaction-scope.enum';
export * from './transaction-type.enum';

View File

@ -0,0 +1,4 @@
export enum TransactionScope {
CARD = 'CARD',
ACCOUNT = 'ACCOUNT',
}

View File

@ -0,0 +1,4 @@
export enum TransactionType {
INTERNAL = 'INTERNAL',
EXTERNAL = 'EXTERNAL',
}

View File

@ -0,0 +1,112 @@
import { UserLocale } from '~/core/enums';
import { CardStatusDescription } from '../enums';
export const CardStatusDescriptionMapper: Record<
CardStatusDescription,
{ [key in UserLocale]: { description: string } }
> = {
[CardStatusDescription.NORMAL]: {
[UserLocale.ENGLISH]: { description: 'The card is active' },
[UserLocale.ARABIC]: { description: 'البطاقة نشطة' },
},
[CardStatusDescription.NOT_YET_ISSUED]: {
[UserLocale.ENGLISH]: { description: 'The card is not yet issued' },
[UserLocale.ARABIC]: { description: 'البطاقة لم تصدر بعد' },
},
[CardStatusDescription.PENDING_ISSUANCE]: {
[UserLocale.ENGLISH]: { description: 'The card is pending issuance' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد الإصدار' },
},
[CardStatusDescription.CARD_EXTRACTED]: {
[UserLocale.ENGLISH]: { description: 'The card has been extracted' },
[UserLocale.ARABIC]: { description: 'تم استخراج البطاقة' },
},
[CardStatusDescription.EXTRACTION_FAILED]: {
[UserLocale.ENGLISH]: { description: 'The card extraction has failed' },
[UserLocale.ARABIC]: { description: 'فشل استخراج البطاقة' },
},
[CardStatusDescription.FAILED_PRINTING_BULK]: {
[UserLocale.ENGLISH]: { description: 'The card printing in bulk has failed' },
[UserLocale.ARABIC]: { description: 'فشل الطباعة بالجملة للبطاقة' },
},
[CardStatusDescription.FAILED_PRINTING_INST]: {
[UserLocale.ENGLISH]: { description: 'The card printing in institution has failed' },
[UserLocale.ARABIC]: { description: 'فشل الطباعة في المؤسسة للبطاقة' },
},
[CardStatusDescription.PENDING_ACTIVATION]: {
[UserLocale.ENGLISH]: { description: 'The card is pending activation' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد التفعيل' },
},
[CardStatusDescription.PENDING_PIN]: {
[UserLocale.ENGLISH]: { description: 'The card is pending PIN' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد الانتظار لرقم التعريف الشخصي' },
},
[CardStatusDescription.PREPARE_TO_CLOSE]: {
[UserLocale.ENGLISH]: { description: 'The card is being prepared for closure' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد التحضير للإغلاق' },
},
[CardStatusDescription.PIN_TRIES_EXCEEDED]: {
[UserLocale.ENGLISH]: { description: 'The card PIN tries have been exceeded' },
[UserLocale.ARABIC]: { description: 'تم تجاوز محاولات رقم التعريف الشخصي للبطاقة' },
},
[CardStatusDescription.CARD_EXPIRED]: {
[UserLocale.ENGLISH]: { description: 'The card has expired' },
[UserLocale.ARABIC]: { description: 'انتهت صلاحية البطاقة' },
},
[CardStatusDescription.LOST]: {
[UserLocale.ENGLISH]: { description: 'The card is lost' },
[UserLocale.ARABIC]: { description: 'البطاقة ضائعة' },
},
[CardStatusDescription.STOLEN]: {
[UserLocale.ENGLISH]: { description: 'The card is stolen' },
[UserLocale.ARABIC]: { description: 'البطاقة مسروقة' },
},
[CardStatusDescription.CUSTOMER_CLOSE]: {
[UserLocale.ENGLISH]: { description: 'The card is being closed by the customer' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد الإغلاق من قبل العميل' },
},
[CardStatusDescription.BANK_CANCELLED]: {
[UserLocale.ENGLISH]: { description: 'The card has been cancelled by the bank' },
[UserLocale.ARABIC]: { description: 'البطاقة ألغيت من قبل البنك' },
},
[CardStatusDescription.FRAUD]: {
[UserLocale.ENGLISH]: { description: 'Fraud' },
[UserLocale.ARABIC]: { description: 'احتيال' },
},
[CardStatusDescription.DAMAGED]: {
[UserLocale.ENGLISH]: { description: 'The card is damaged' },
[UserLocale.ARABIC]: { description: 'البطاقة تالفة' },
},
[CardStatusDescription.SAFE_BLOCK]: {
[UserLocale.ENGLISH]: { description: 'The card is in a safe block' },
[UserLocale.ARABIC]: { description: 'البطاقة في حظر آمن' },
},
[CardStatusDescription.TEMPORARY_BLOCK]: {
[UserLocale.ENGLISH]: { description: 'The card is in a temporary block' },
[UserLocale.ARABIC]: { description: 'البطاقة في حظر مؤقت' },
},
[CardStatusDescription.RISK_BLOCK]: {
[UserLocale.ENGLISH]: { description: 'The card is in a risk block' },
[UserLocale.ARABIC]: { description: 'البطاقة في حظر المخاطر' },
},
[CardStatusDescription.OVERDRAFT]: {
[UserLocale.ENGLISH]: { description: 'The card is in overdraft' },
[UserLocale.ARABIC]: { description: 'البطاقة في السحب على المكشوف' },
},
[CardStatusDescription.BLOCKED_FOR_FEES]: {
[UserLocale.ENGLISH]: { description: 'The card is blocked for fees' },
[UserLocale.ARABIC]: { description: 'البطاقة محظورة للرسوم' },
},
[CardStatusDescription.CLOSED_CUSTOMER_DEAD]: {
[UserLocale.ENGLISH]: { description: 'The card is closed because the customer is dead' },
[UserLocale.ARABIC]: { description: 'البطاقة مغلقة لأن العميل متوفى' },
},
[CardStatusDescription.RETURN_CARD]: {
[UserLocale.ENGLISH]: { description: 'The card is being returned' },
[UserLocale.ARABIC]: { description: 'البطاقة قيد الإرجاع' },
},
[CardStatusDescription.UNKNOWN]: {
[UserLocale.ENGLISH]: { description: 'The card status is unknown' },
[UserLocale.ARABIC]: { description: 'حالة البطاقة غير معروفة' },
},
};

View File

@ -0,0 +1,37 @@
import { CardStatus, CardStatusDescription } from '../enums';
export const CardStatusMapper: Record<string, { description: CardStatusDescription; status: CardStatus }> = {
//ACTIVE
'00': { description: CardStatusDescription.NORMAL, status: CardStatus.ACTIVE },
//PENDING
'02': { description: CardStatusDescription.NOT_YET_ISSUED, status: CardStatus.PENDING },
'20': { description: CardStatusDescription.PENDING_ISSUANCE, status: CardStatus.PENDING },
'21': { description: CardStatusDescription.CARD_EXTRACTED, status: CardStatus.PENDING },
'22': { description: CardStatusDescription.EXTRACTION_FAILED, status: CardStatus.PENDING },
'23': { description: CardStatusDescription.FAILED_PRINTING_BULK, status: CardStatus.PENDING },
'24': { description: CardStatusDescription.FAILED_PRINTING_INST, status: CardStatus.PENDING },
'30': { description: CardStatusDescription.PENDING_ACTIVATION, status: CardStatus.PENDING },
'27': { description: CardStatusDescription.PENDING_PIN, status: CardStatus.PENDING },
'16': { description: CardStatusDescription.PREPARE_TO_CLOSE, status: CardStatus.PENDING },
//BLOCKED
'01': { description: CardStatusDescription.PIN_TRIES_EXCEEDED, status: CardStatus.BLOCKED },
'03': { description: CardStatusDescription.CARD_EXPIRED, status: CardStatus.BLOCKED },
'04': { description: CardStatusDescription.LOST, status: CardStatus.BLOCKED },
'05': { description: CardStatusDescription.STOLEN, status: CardStatus.BLOCKED },
'06': { description: CardStatusDescription.CUSTOMER_CLOSE, status: CardStatus.BLOCKED },
'07': { description: CardStatusDescription.BANK_CANCELLED, status: CardStatus.BLOCKED },
'08': { description: CardStatusDescription.FRAUD, status: CardStatus.BLOCKED },
'09': { description: CardStatusDescription.DAMAGED, status: CardStatus.BLOCKED },
'50': { description: CardStatusDescription.SAFE_BLOCK, status: CardStatus.BLOCKED },
'51': { description: CardStatusDescription.TEMPORARY_BLOCK, status: CardStatus.BLOCKED },
'52': { description: CardStatusDescription.RISK_BLOCK, status: CardStatus.BLOCKED },
'53': { description: CardStatusDescription.OVERDRAFT, status: CardStatus.BLOCKED },
'54': { description: CardStatusDescription.BLOCKED_FOR_FEES, status: CardStatus.BLOCKED },
'67': { description: CardStatusDescription.CLOSED_CUSTOMER_DEAD, status: CardStatus.BLOCKED },
'75': { description: CardStatusDescription.RETURN_CARD, status: CardStatus.BLOCKED },
//Fallback
'99': { description: CardStatusDescription.UNKNOWN, status: CardStatus.PENDING },
};

View File

@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateApplicationResponse } from '~/common/modules/neoleap/dtos/response';
import { Account } from '../entities/account.entity';
@Injectable()
export class AccountRepository {
constructor(@InjectRepository(Account) private readonly accountRepository: Repository<Account>) {}
createAccount(data: CreateApplicationResponse): Promise<Account> {
return this.accountRepository.save(
this.accountRepository.create({
accountReference: data.accountId,
accountNumber: data.accountNumber,
iban: data.iBan,
balance: 0,
currency: '682',
}),
);
}
getAccountByReferenceNumber(accountReference: string): Promise<Account | null> {
return this.accountRepository.findOne({
where: { accountReference },
relations: ['cards'],
});
}
getAccountByAccountNumber(accountNumber: string): Promise<Account | null> {
return this.accountRepository.findOne({
where: { accountNumber },
relations: ['cards'],
});
}
topUpAccountBalance(accountReference: string, amount: number) {
return this.accountRepository.increment({ accountReference }, 'balance', amount);
}
decreaseAccountBalance(accountReference: string, amount: number) {
return this.accountRepository.decrement({ accountReference }, 'balance', amount);
}
}

View File

@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateApplicationResponse } from '~/common/modules/neoleap/dtos/response';
import { Card } from '../entities';
import { CardColors, CardIssuers, CardScheme, CardStatus, CardStatusDescription, CustomerType } from '../enums';
@Injectable()
export class CardRepository {
constructor(@InjectRepository(Card) private readonly cardRepository: Repository<Card>) {}
createCard(customerId: string, accountId: string, card: CreateApplicationResponse): Promise<Card> {
return this.cardRepository.save(
this.cardRepository.create({
customerId: customerId,
expiry: card.expiryDate,
cardReference: card.cardId,
customerType: CustomerType.PARENT,
firstSixDigits: card.firstSixDigits,
lastFourDigits: card.lastFourDigits,
color: CardColors.BLUE,
scheme: CardScheme.VISA,
issuer: CardIssuers.NEOLEAP,
accountId: accountId,
vpan: card.vpan,
}),
);
}
getCardById(id: string): Promise<Card | null> {
return this.cardRepository.findOne({ where: { id }, relations: ['account'] });
}
getCardByReferenceNumber(referenceNumber: string): Promise<Card | null> {
return this.cardRepository.findOne({ where: { cardReference: referenceNumber }, relations: ['account'] });
}
getCardByVpan(vpan: string): Promise<Card | null> {
return this.cardRepository.findOne({
where: { vpan },
relations: ['account'],
});
}
getCardByCustomerId(customerId: string): Promise<Card | null> {
return this.cardRepository.findOne({
where: { customerId },
relations: ['account'],
});
}
updateCardStatus(id: string, status: CardStatus, statusDescription: CardStatusDescription) {
return this.cardRepository.update(id, {
status: status,
statusDescription: statusDescription,
});
}
}

View File

@ -0,0 +1 @@
export * from './card.repository';

View File

@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import moment from 'moment';
import { Repository } from 'typeorm';
import {
AccountTransactionWebhookRequest,
CardTransactionWebhookRequest,
} from '~/common/modules/neoleap/dtos/requests';
import { Card } from '../entities';
import { Account } from '../entities/account.entity';
import { Transaction } from '../entities/transaction.entity';
import { TransactionScope, TransactionType } from '../enums';
@Injectable()
export class TransactionRepository {
constructor(@InjectRepository(Transaction) private transactionRepository: Repository<Transaction>) {}
createCardTransaction(card: Card, transactionData: CardTransactionWebhookRequest): Promise<Transaction> {
return this.transactionRepository.save(
this.transactionRepository.create({
transactionId: transactionData.transactionId,
cardReference: transactionData.cardId,
transactionAmount: transactionData.transactionAmount,
transactionCurrency: transactionData.transactionCurrency,
billingAmount: transactionData.billingAmount,
settlementAmount: transactionData.settlementAmount,
transactionDate: moment(transactionData.date + transactionData.time, 'YYYYMMDDHHmmss').toDate(),
rrn: transactionData.rrn,
cardMaskedNumber: transactionData.cardMaskedNumber,
fees: transactionData.fees,
cardId: card.id,
accountId: card.account!.id,
transactionType: TransactionType.EXTERNAL,
accountReference: card.account!.accountReference,
transactionScope: TransactionScope.CARD,
vatOnFees: transactionData.vatOnFees,
}),
);
}
createAccountTransaction(account: Account, transactionData: AccountTransactionWebhookRequest): Promise<Transaction> {
return this.transactionRepository.save(
this.transactionRepository.create({
transactionId: transactionData.transactionId,
transactionAmount: transactionData.amount,
transactionCurrency: transactionData.currency,
billingAmount: 0,
settlementAmount: 0,
transactionDate: moment(transactionData.date + transactionData.time, 'YYYYMMDDHHmmss').toDate(),
fees: 0,
accountReference: account.accountReference,
accountId: account.id,
transactionType: TransactionType.EXTERNAL,
transactionScope: TransactionScope.ACCOUNT,
vatOnFees: 0,
}),
);
}
findTransactionByReference(transactionId: string, accountReference: string): Promise<Transaction | null> {
return this.transactionRepository.findOne({
where: { transactionId, accountReference },
});
}
}

View File

@ -0,0 +1,47 @@
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { CreateApplicationResponse } from '~/common/modules/neoleap/dtos/response';
import { Account } from '../entities/account.entity';
import { AccountRepository } from '../repositories/account.repository';
@Injectable()
export class AccountService {
constructor(private readonly accountRepository: AccountRepository) {}
createAccount(data: CreateApplicationResponse): Promise<Account> {
return this.accountRepository.createAccount(data);
}
async getAccountByReferenceNumber(accountReference: string): Promise<Account> {
const account = await this.accountRepository.getAccountByReferenceNumber(accountReference);
if (!account) {
throw new UnprocessableEntityException('ACCOUNT.NOT_FOUND');
}
return account;
}
async getAccountByAccountNumber(accountNumber: string): Promise<Account> {
const account = await this.accountRepository.getAccountByAccountNumber(accountNumber);
if (!account) {
throw new UnprocessableEntityException('ACCOUNT.NOT_FOUND');
}
return account;
}
async creditAccountBalance(accountReference: string, amount: number) {
return this.accountRepository.topUpAccountBalance(accountReference, amount);
}
async decreaseAccountBalance(accountReference: string, amount: number) {
const account = await this.getAccountByReferenceNumber(accountReference);
/**
* While there is no need to check for insufficient balance because this is a webhook handler,
* I just added this check to ensure we don't have corruption in our data especially if this service is used elsewhere.
*/
if (account.balance < amount) {
throw new UnprocessableEntityException('ACCOUNT.INSUFFICIENT_BALANCE');
}
return this.accountRepository.decreaseAccountBalance(accountReference, amount);
}
}

View File

@ -0,0 +1,89 @@
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { Transactional } from 'typeorm-transactional';
import { AccountCardStatusChangedWebhookRequest } from '~/common/modules/neoleap/dtos/requests';
import { NeoLeapService } from '~/common/modules/neoleap/services';
import { KycStatus } from '~/customer/enums';
import { CustomerService } from '~/customer/services';
import { Card } from '../entities';
import { CardStatusMapper } from '../mappers/card-status.mapper';
import { CardRepository } from '../repositories';
import { AccountService } from './account.service';
@Injectable()
export class CardService {
constructor(
private readonly cardRepository: CardRepository,
private readonly accountService: AccountService,
@Inject(forwardRef(() => NeoLeapService)) private readonly neoleapService: NeoLeapService,
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
) {}
@Transactional()
async createCard(customerId: string): Promise<Card> {
const customer = await this.customerService.findCustomerById(customerId);
if (customer.kycStatus !== KycStatus.APPROVED) {
throw new BadRequestException('CUSTOMER.KYC_NOT_APPROVED');
}
if (customer.cards.length > 0) {
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
}
const data = await this.neoleapService.createApplication(customer);
const account = await this.accountService.createAccount(data);
const createdCard = await this.cardRepository.createCard(customerId, account.id, data);
return this.getCardById(createdCard.id);
}
async getCardById(id: string): Promise<Card> {
const card = await this.cardRepository.getCardById(id);
if (!card) {
throw new BadRequestException('CARD.NOT_FOUND');
}
return card;
}
async getCardByReferenceNumber(referenceNumber: string): Promise<Card> {
const card = await this.cardRepository.getCardByReferenceNumber(referenceNumber);
if (!card) {
throw new BadRequestException('CARD.NOT_FOUND');
}
return card;
}
async getCardByVpan(vpan: string): Promise<Card> {
const card = await this.cardRepository.getCardByVpan(vpan);
if (!card) {
throw new BadRequestException('CARD.NOT_FOUND');
}
return card;
}
async getCardByCustomerId(customerId: string): Promise<Card> {
const card = await this.cardRepository.getCardByCustomerId(customerId);
if (!card) {
throw new BadRequestException('CARD.NOT_FOUND');
}
return card;
}
async updateCardStatus(body: AccountCardStatusChangedWebhookRequest) {
const card = await this.getCardByVpan(body.cardId);
const { description, status } = CardStatusMapper[body.newStatus] || CardStatusMapper['99'];
return this.cardRepository.updateCardStatus(card.id, status, description);
}
async getEmbossingInformation(customerId: string) {
const card = await this.getCardByCustomerId(customerId);
return this.neoleapService.getEmbossingInformation(card);
}
}

View File

@ -0,0 +1 @@
export * from './card.service';

View File

@ -0,0 +1,62 @@
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import Decimal from 'decimal.js';
import { Transactional } from 'typeorm-transactional';
import {
AccountTransactionWebhookRequest,
CardTransactionWebhookRequest,
} from '~/common/modules/neoleap/dtos/requests';
import { Transaction } from '../entities/transaction.entity';
import { TransactionRepository } from '../repositories/transaction.repository';
import { AccountService } from './account.service';
import { CardService } from './card.service';
@Injectable()
export class TransactionService {
constructor(
private readonly transactionRepository: TransactionRepository,
private readonly accountService: AccountService,
private readonly cardService: CardService,
) {}
@Transactional()
async createCardTransaction(body: CardTransactionWebhookRequest) {
const card = await this.cardService.getCardByVpan(body.cardId);
const existingTransaction = await this.findExistingTransaction(body.transactionId, card.account.accountReference);
if (existingTransaction) {
throw new UnprocessableEntityException('TRANSACTION.ALREADY_EXISTS');
}
const transaction = await this.transactionRepository.createCardTransaction(card, body);
const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees);
await this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber());
return transaction;
}
@Transactional()
async createAccountTransaction(body: AccountTransactionWebhookRequest) {
const account = await this.accountService.getAccountByAccountNumber(body.accountId);
const existingTransaction = await this.findExistingTransaction(body.transactionId, account.accountReference);
if (existingTransaction) {
throw new UnprocessableEntityException('TRANSACTION.ALREADY_EXISTS');
}
const transaction = await this.transactionRepository.createAccountTransaction(account, body);
await this.accountService.creditAccountBalance(account.accountReference, body.amount);
return transaction;
}
private async findExistingTransaction(transactionId: string, accountReference: string): Promise<Transaction | null> {
const existingTransaction = await this.transactionRepository.findTransactionByReference(
transactionId,
accountReference,
);
return existingTransaction;
}
}

View File

@ -0,0 +1,253 @@
import { CountryIso } from '../enums';
export const CountriesNumericISO: Record<CountryIso, string> = {
[CountryIso.ARUBA]: '533',
[CountryIso.AFGHANISTAN]: '004',
[CountryIso.ANGOLA]: '024',
[CountryIso.ANGUILLA]: '660',
[CountryIso.ALAND_ISLANDS]: '248',
[CountryIso.ALBANIA]: '008',
[CountryIso.ANDORRA]: '020',
[CountryIso.UNITED_ARAB_EMIRATES]: '784',
[CountryIso.ARGENTINA]: '032',
[CountryIso.ARMENIA]: '051',
[CountryIso.AMERICAN_SAMOA]: '016',
[CountryIso.ANTARCTICA]: '010',
[CountryIso.FRENCH_SOUTHERN_TERRITORIES]: '260',
[CountryIso.ANTIGUA_AND_BARBUDA]: '028',
[CountryIso.AUSTRALIA]: '036',
[CountryIso.AUSTRIA]: '040',
[CountryIso.AZERBAIJAN]: '031',
[CountryIso.BURUNDI]: '108',
[CountryIso.BELGIUM]: '056',
[CountryIso.BENIN]: '204',
[CountryIso.BONAIRE_SINT_EUSTATIUS_AND_SABA]: '535',
[CountryIso.BURKINA_FASO]: '854',
[CountryIso.BANGLADESH]: '050',
[CountryIso.BULGARIA]: '100',
[CountryIso.BAHRAIN]: '048',
[CountryIso.BAHAMAS]: '044',
[CountryIso.BOSNIA_AND_HERZEGOVINA]: '070',
[CountryIso.SAINT_BARTHÉLEMY]: '652',
[CountryIso.BELARUS]: '112',
[CountryIso.BELIZE]: '084',
[CountryIso.BERMUDA]: '060',
[CountryIso.BOLIVIA_PLURINATIONAL_STATE_OF]: '068',
[CountryIso.BRAZIL]: '076',
[CountryIso.BARBADOS]: '052',
[CountryIso.BRUNEI_DARUSSALAM]: '096',
[CountryIso.BHUTAN]: '064',
[CountryIso.BOUVET_ISLAND]: '074',
[CountryIso.BOTSWANA]: '072',
[CountryIso.CENTRAL_AFRICAN_REPUBLIC]: '140',
[CountryIso.CANADA]: '124',
[CountryIso.COCOS_KEELING_ISLANDS]: '166',
[CountryIso.SWITZERLAND]: '756',
[CountryIso.CHILE]: '152',
[CountryIso.CHINA]: '156',
[CountryIso.COTE_DIVOIRE]: '384',
[CountryIso.CAMEROON]: '120',
[CountryIso.CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE]: '180',
[CountryIso.CONGO]: '178',
[CountryIso.COOK_ISLANDS]: '184',
[CountryIso.COLOMBIA]: '170',
[CountryIso.COMOROS]: '174',
[CountryIso.CABO_VERDE]: '132',
[CountryIso.COSTA_RICA]: '188',
[CountryIso.CUBA]: '192',
[CountryIso.CURAÇAO]: '531',
[CountryIso.CHRISTMAS_ISLAND]: '162',
[CountryIso.CAYMAN_ISLANDS]: '136',
[CountryIso.CYPRUS]: '196',
[CountryIso.CZECHIA]: '203',
[CountryIso.GERMANY]: '276',
[CountryIso.DJIBOUTI]: '262',
[CountryIso.DOMINICA]: '212',
[CountryIso.DENMARK]: '208',
[CountryIso.DOMINICAN_REPUBLIC]: '214',
[CountryIso.ALGERIA]: '012',
[CountryIso.ECUADOR]: '218',
[CountryIso.EGYPT]: '818',
[CountryIso.ERITREA]: '232',
[CountryIso.WESTERN_SAHARA]: '732',
[CountryIso.SPAIN]: '724',
[CountryIso.ESTONIA]: '233',
[CountryIso.ETHIOPIA]: '231',
[CountryIso.FINLAND]: '246',
[CountryIso.FIJI]: '242',
[CountryIso.FALKLAND_ISLANDS_MALVINAS]: '238',
[CountryIso.FRANCE]: '250',
[CountryIso.FAROE_ISLANDS]: '234',
[CountryIso.MICRONESIA_FEDERATED_STATES_OF]: '583',
[CountryIso.GABON]: '266',
[CountryIso.UNITED_KINGDOM]: '826',
[CountryIso.GEORGIA]: '268',
[CountryIso.GUERNSEY]: '831',
[CountryIso.GHANA]: '288',
[CountryIso.GIBRALTAR]: '292',
[CountryIso.GUINEA]: '324',
[CountryIso.GUADELOUPE]: '312',
[CountryIso.GAMBIA]: '270',
[CountryIso.GUINEA_BISSAU]: '624',
[CountryIso.EQUATORIAL_GUINEA]: '226',
[CountryIso.GREECE]: '300',
[CountryIso.GRENADA]: '308',
[CountryIso.GREENLAND]: '304',
[CountryIso.GUATEMALA]: '320',
[CountryIso.FRENCH_GUIANA]: '254',
[CountryIso.GUAM]: '316',
[CountryIso.GUYANA]: '328',
[CountryIso.HONG_KONG]: '344',
[CountryIso.HEARD_ISLAND_AND_MCDONALD_ISLANDS]: '334',
[CountryIso.HONDURAS]: '340',
[CountryIso.CROATIA]: '191',
[CountryIso.HAITI]: '332',
[CountryIso.HUNGARY]: '348',
[CountryIso.INDONESIA]: '360',
[CountryIso.ISLE_OF_MAN]: '833',
[CountryIso.INDIA]: '356',
[CountryIso.BRITISH_INDIAN_OCEAN_TERRITORY]: '086',
[CountryIso.IRELAND]: '372',
[CountryIso.IRAN_ISLAMIC_REPUBLIC_OF]: '364',
[CountryIso.IRAQ]: '368',
[CountryIso.ICELAND]: '352',
[CountryIso.ISRAEL]: '376',
[CountryIso.ITALY]: '380',
[CountryIso.JAMAICA]: '388',
[CountryIso.JERSEY]: '832',
[CountryIso.JORDAN]: '400',
[CountryIso.JAPAN]: '392',
[CountryIso.KAZAKHSTAN]: '398',
[CountryIso.KENYA]: '404',
[CountryIso.KYRGYZSTAN]: '417',
[CountryIso.CAMBODIA]: '116',
[CountryIso.KIRIBATI]: '296',
[CountryIso.SAINT_KITTS_AND_NEVIS]: '659',
[CountryIso.KOREA_REPUBLIC_OF]: '410',
[CountryIso.KUWAIT]: '414',
[CountryIso.LAO_PEOPLES_DEMOCRATIC_REPUBLIC]: '418',
[CountryIso.LEBANON]: '422',
[CountryIso.LIBERIA]: '430',
[CountryIso.LIBYA]: '434',
[CountryIso.SAINT_LUCIA]: '662',
[CountryIso.LIECHTENSTEIN]: '438',
[CountryIso.SRI_LANKA]: '144',
[CountryIso.LESOTHO]: '426',
[CountryIso.LITHUANIA]: '440',
[CountryIso.LUXEMBOURG]: '442',
[CountryIso.LATVIA]: '428',
[CountryIso.MACAO]: '446',
[CountryIso.SAINT_MARTIN_FRENCH_PART]: '663',
[CountryIso.MOROCCO]: '504',
[CountryIso.MONACO]: '492',
[CountryIso.MOLDOVA_REPUBLIC_OF]: '498',
[CountryIso.MADAGASCAR]: '450',
[CountryIso.MALDIVES]: '462',
[CountryIso.MEXICO]: '484',
[CountryIso.MARSHALL_ISLANDS]: '584',
[CountryIso.NORTH_MACEDONIA]: '807',
[CountryIso.MALI]: '466',
[CountryIso.MALTA]: '470',
[CountryIso.MYANMAR]: '104',
[CountryIso.MONTENEGRO]: '499',
[CountryIso.MONGOLIA]: '496',
[CountryIso.NORTHERN_MARIANA_ISLANDS]: '580',
[CountryIso.MOZAMBIQUE]: '508',
[CountryIso.MAURITANIA]: '478',
[CountryIso.MONTSERRAT]: '500',
[CountryIso.MARTINIQUE]: '474',
[CountryIso.MAURITIUS]: '480',
[CountryIso.MALAWI]: '454',
[CountryIso.MALAYSIA]: '458',
[CountryIso.MAYOTTE]: '175',
[CountryIso.NAMIBIA]: '516',
[CountryIso.NEW_CALEDONIA]: '540',
[CountryIso.NIGER]: '562',
[CountryIso.NORFOLK_ISLAND]: '574',
[CountryIso.NIGERIA]: '566',
[CountryIso.NICARAGUA]: '558',
[CountryIso.NIUE]: '570',
[CountryIso.NETHERLANDS]: '528',
[CountryIso.NORWAY]: '578',
[CountryIso.NEPAL]: '524',
[CountryIso.NAURU]: '520',
[CountryIso.NEW_ZEALAND]: '554',
[CountryIso.OMAN]: '512',
[CountryIso.PAKISTAN]: '586',
[CountryIso.PANAMA]: '591',
[CountryIso.PITCAIRN]: '612',
[CountryIso.PERU]: '604',
[CountryIso.PHILIPPINES]: '608',
[CountryIso.PALAU]: '585',
[CountryIso.PAPUA_NEW_GUINEA]: '598',
[CountryIso.POLAND]: '616',
[CountryIso.PUERTO_RICO]: '630',
[CountryIso.KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF]: '408',
[CountryIso.PORTUGAL]: '620',
[CountryIso.PARAGUAY]: '600',
[CountryIso.PALESTINE_STATE_OF]: '275',
[CountryIso.FRENCH_POLYNESIA]: '258',
[CountryIso.QATAR]: '634',
[CountryIso.REUNION]: '638',
[CountryIso.ROMANIA]: '642',
[CountryIso.RUSSIAN_FEDERATION]: '643',
[CountryIso.RWANDA]: '646',
[CountryIso.SAUDI_ARABIA]: '682',
[CountryIso.SUDAN]: '729',
[CountryIso.SENEGAL]: '686',
[CountryIso.SINGAPORE]: '702',
[CountryIso.SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS]: '239',
[CountryIso.SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA]: '654',
[CountryIso.SVALBARD_AND_JAN_MAYEN]: '744',
[CountryIso.SOLOMON_ISLANDS]: '090',
[CountryIso.SIERRA_LEONE]: '694',
[CountryIso.EL_SALVADOR]: '222',
[CountryIso.SAN_MARINO]: '674',
[CountryIso.SOMALIA]: '706',
[CountryIso.SAINT_PIERRE_AND_MIQUELON]: '666',
[CountryIso.SERBIA]: '688',
[CountryIso.SOUTH_SUDAN]: '728',
[CountryIso.SAO_TOME_AND_PRINCIPE]: '678',
[CountryIso.SURINAME]: '740',
[CountryIso.SLOVAKIA]: '703',
[CountryIso.SLOVENIA]: '705',
[CountryIso.SWEDEN]: '752',
[CountryIso.ESWATINI]: '748',
[CountryIso.SINT_MAARTEN_DUTCH_PART]: '534',
[CountryIso.SEYCHELLES]: '690',
[CountryIso.SYRIAN_ARAB_REPUBLIC]: '760',
[CountryIso.TURKS_AND_CAICOS_ISLANDS]: '796',
[CountryIso.CHAD]: '148',
[CountryIso.TOGO]: '768',
[CountryIso.THAILAND]: '764',
[CountryIso.TAJIKISTAN]: '762',
[CountryIso.TOKELAU]: '772',
[CountryIso.TURKMENISTAN]: '795',
[CountryIso.TIMOR_LESTE]: '626',
[CountryIso.TONGA]: '776',
[CountryIso.TRINIDAD_AND_TOBAGO]: '780',
[CountryIso.TUNISIA]: '788',
[CountryIso.TURKEY]: '792',
[CountryIso.TUVALU]: '798',
[CountryIso.TAIWAN_PROVINCE_OF_CHINA]: '158',
[CountryIso.TANZANIA_UNITED_REPUBLIC_OF]: '834',
[CountryIso.UGANDA]: '800',
[CountryIso.UKRAINE]: '804',
[CountryIso.UNITED_STATES_MINOR_OUTLYING_ISLANDS]: '581',
[CountryIso.URUGUAY]: '858',
[CountryIso.UNITED_STATES]: '840',
[CountryIso.UZBEKISTAN]: '860',
[CountryIso.HOLY_SEE_VATICAN_CITY_STATE]: '336',
[CountryIso.SAINT_VINCENT_AND_THE_GRENADINES]: '670',
[CountryIso.VENEZUELA_BOLIVARIAN_REPUBLIC_OF]: '862',
[CountryIso.VIRGIN_ISLANDS_BRITISH]: '092',
[CountryIso.VIRGIN_ISLANDS_US]: '850',
[CountryIso.VIET_NAM]: '704',
[CountryIso.VANUATU]: '548',
[CountryIso.WALLIS_AND_FUTUNA]: '876',
[CountryIso.SAMOA]: '882',
[CountryIso.YEMEN]: '887',
[CountryIso.SOUTH_AFRICA]: '710',
[CountryIso.ZAMBIA]: '894',
[CountryIso.ZIMBABWE]: '716',
};

View File

@ -1 +1,2 @@
export * from './countries-numeric-iso.constant';
export * from './global.constant';

View File

@ -0,0 +1,251 @@
export enum CountryIso {
ARUBA = 'AW',
AFGHANISTAN = 'AF',
ANGOLA = 'AO',
ANGUILLA = 'AI',
ALAND_ISLANDS = 'AX',
ALBANIA = 'AL',
ANDORRA = 'AD',
UNITED_ARAB_EMIRATES = 'AE',
ARGENTINA = 'AR',
ARMENIA = 'AM',
AMERICAN_SAMOA = 'AS',
ANTARCTICA = 'AQ',
FRENCH_SOUTHERN_TERRITORIES = 'TF',
ANTIGUA_AND_BARBUDA = 'AG',
AUSTRALIA = 'AU',
AUSTRIA = 'AT',
AZERBAIJAN = 'AZ',
BURUNDI = 'BI',
BELGIUM = 'BE',
BENIN = 'BJ',
BONAIRE_SINT_EUSTATIUS_AND_SABA = 'BQ',
BURKINA_FASO = 'BF',
BANGLADESH = 'BD',
BULGARIA = 'BG',
BAHRAIN = 'BH',
BAHAMAS = 'BS',
BOSNIA_AND_HERZEGOVINA = 'BA',
SAINT_BARTHÉLEMY = 'BL',
BELARUS = 'BY',
BELIZE = 'BZ',
BERMUDA = 'BM',
BOLIVIA_PLURINATIONAL_STATE_OF = 'BO',
BRAZIL = 'BR',
BARBADOS = 'BB',
BRUNEI_DARUSSALAM = 'BN',
BHUTAN = 'BT',
BOUVET_ISLAND = 'BV',
BOTSWANA = 'BW',
CENTRAL_AFRICAN_REPUBLIC = 'CF',
CANADA = 'CA',
COCOS_KEELING_ISLANDS = 'CC',
SWITZERLAND = 'CH',
CHILE = 'CL',
CHINA = 'CN',
COTE_DIVOIRE = 'CI',
CAMEROON = 'CM',
CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE = 'CD',
CONGO = 'CG',
COOK_ISLANDS = 'CK',
COLOMBIA = 'CO',
COMOROS = 'KM',
CABO_VERDE = 'CV',
COSTA_RICA = 'CR',
CUBA = 'CU',
CURAÇAO = 'CW',
CHRISTMAS_ISLAND = 'CX',
CAYMAN_ISLANDS = 'KY',
CYPRUS = 'CY',
CZECHIA = 'CZ',
GERMANY = 'DE',
DJIBOUTI = 'DJ',
DOMINICA = 'DM',
DENMARK = 'DK',
DOMINICAN_REPUBLIC = 'DO',
ALGERIA = 'DZ',
ECUADOR = 'EC',
EGYPT = 'EG',
ERITREA = 'ER',
WESTERN_SAHARA = 'EH',
SPAIN = 'ES',
ESTONIA = 'EE',
ETHIOPIA = 'ET',
FINLAND = 'FI',
FIJI = 'FJ',
FALKLAND_ISLANDS_MALVINAS = 'FK',
FRANCE = 'FR',
FAROE_ISLANDS = 'FO',
MICRONESIA_FEDERATED_STATES_OF = 'FM',
GABON = 'GA',
UNITED_KINGDOM = 'GB',
GEORGIA = 'GE',
GUERNSEY = 'GG',
GHANA = 'GH',
GIBRALTAR = 'GI',
GUINEA = 'GN',
GUADELOUPE = 'GP',
GAMBIA = 'GM',
GUINEA_BISSAU = 'GW',
EQUATORIAL_GUINEA = 'GQ',
GREECE = 'GR',
GRENADA = 'GD',
GREENLAND = 'GL',
GUATEMALA = 'GT',
FRENCH_GUIANA = 'GF',
GUAM = 'GU',
GUYANA = 'GY',
HONG_KONG = 'HK',
HEARD_ISLAND_AND_MCDONALD_ISLANDS = 'HM',
HONDURAS = 'HN',
CROATIA = 'HR',
HAITI = 'HT',
HUNGARY = 'HU',
INDONESIA = 'ID',
ISLE_OF_MAN = 'IM',
INDIA = 'IN',
BRITISH_INDIAN_OCEAN_TERRITORY = 'IO',
IRELAND = 'IE',
IRAN_ISLAMIC_REPUBLIC_OF = 'IR',
IRAQ = 'IQ',
ICELAND = 'IS',
ISRAEL = 'IL',
ITALY = 'IT',
JAMAICA = 'JM',
JERSEY = 'JE',
JORDAN = 'JO',
JAPAN = 'JP',
KAZAKHSTAN = 'KZ',
KENYA = 'KE',
KYRGYZSTAN = 'KG',
CAMBODIA = 'KH',
KIRIBATI = 'KI',
SAINT_KITTS_AND_NEVIS = 'KN',
KOREA_REPUBLIC_OF = 'KR',
KUWAIT = 'KW',
LAO_PEOPLES_DEMOCRATIC_REPUBLIC = 'LA',
LEBANON = 'LB',
LIBERIA = 'LR',
LIBYA = 'LY',
SAINT_LUCIA = 'LC',
LIECHTENSTEIN = 'LI',
SRI_LANKA = 'LK',
LESOTHO = 'LS',
LITHUANIA = 'LT',
LUXEMBOURG = 'LU',
LATVIA = 'LV',
MACAO = 'MO',
SAINT_MARTIN_FRENCH_PART = 'MF',
MOROCCO = 'MA',
MONACO = 'MC',
MOLDOVA_REPUBLIC_OF = 'MD',
MADAGASCAR = 'MG',
MALDIVES = 'MV',
MEXICO = 'MX',
MARSHALL_ISLANDS = 'MH',
NORTH_MACEDONIA = 'MK',
MALI = 'ML',
MALTA = 'MT',
MYANMAR = 'MM',
MONTENEGRO = 'ME',
MONGOLIA = 'MN',
NORTHERN_MARIANA_ISLANDS = 'MP',
MOZAMBIQUE = 'MZ',
MAURITANIA = 'MR',
MONTSERRAT = 'MS',
MARTINIQUE = 'MQ',
MAURITIUS = 'MU',
MALAWI = 'MW',
MALAYSIA = 'MY',
MAYOTTE = 'YT',
NAMIBIA = 'NA',
NEW_CALEDONIA = 'NC',
NIGER = 'NE',
NORFOLK_ISLAND = 'NF',
NIGERIA = 'NG',
NICARAGUA = 'NI',
NIUE = 'NU',
NETHERLANDS = 'NL',
NORWAY = 'NO',
NEPAL = 'NP',
NAURU = 'NR',
NEW_ZEALAND = 'NZ',
OMAN = 'OM',
PAKISTAN = 'PK',
PANAMA = 'PA',
PITCAIRN = 'PN',
PERU = 'PE',
PHILIPPINES = 'PH',
PALAU = 'PW',
PAPUA_NEW_GUINEA = 'PG',
POLAND = 'PL',
PUERTO_RICO = 'PR',
KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF = 'KP',
PORTUGAL = 'PT',
PARAGUAY = 'PY',
PALESTINE_STATE_OF = 'PS',
FRENCH_POLYNESIA = 'PF',
QATAR = 'QA',
REUNION = 'RE',
ROMANIA = 'RO',
RUSSIAN_FEDERATION = 'RU',
RWANDA = 'RW',
SAUDI_ARABIA = 'SA',
SUDAN = 'SD',
SENEGAL = 'SN',
SINGAPORE = 'SG',
SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS = 'GS',
SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA = 'SH',
SVALBARD_AND_JAN_MAYEN = 'SJ',
SOLOMON_ISLANDS = 'SB',
SIERRA_LEONE = 'SL',
EL_SALVADOR = 'SV',
SAN_MARINO = 'SM',
SOMALIA = 'SO',
SAINT_PIERRE_AND_MIQUELON = 'PM',
SERBIA = 'RS',
SOUTH_SUDAN = 'SS',
SAO_TOME_AND_PRINCIPE = 'ST',
SURINAME = 'SR',
SLOVAKIA = 'SK',
SLOVENIA = 'SI',
SWEDEN = 'SE',
ESWATINI = 'SZ',
SINT_MAARTEN_DUTCH_PART = 'SX',
SEYCHELLES = 'SC',
SYRIAN_ARAB_REPUBLIC = 'SY',
TURKS_AND_CAICOS_ISLANDS = 'TC',
CHAD = 'TD',
TOGO = 'TG',
THAILAND = 'TH',
TAJIKISTAN = 'TJ',
TOKELAU = 'TK',
TURKMENISTAN = 'TM',
TIMOR_LESTE = 'TL',
TONGA = 'TO',
TRINIDAD_AND_TOBAGO = 'TT',
TUNISIA = 'TN',
TURKEY = 'TR',
TUVALU = 'TV',
TAIWAN_PROVINCE_OF_CHINA = 'TW',
TANZANIA_UNITED_REPUBLIC_OF = 'TZ',
UGANDA = 'UG',
UKRAINE = 'UA',
UNITED_STATES_MINOR_OUTLYING_ISLANDS = 'UM',
URUGUAY = 'UY',
UNITED_STATES = 'US',
UZBEKISTAN = 'UZ',
HOLY_SEE_VATICAN_CITY_STATE = 'VA',
SAINT_VINCENT_AND_THE_GRENADINES = 'VC',
VENEZUELA_BOLIVARIAN_REPUBLIC_OF = 'VE',
VIRGIN_ISLANDS_BRITISH = 'VG',
VIRGIN_ISLANDS_US = 'VI',
VIET_NAM = 'VN',
VANUATU = 'VU',
WALLIS_AND_FUTUNA = 'WF',
SAMOA = 'WS',
YEMEN = 'YE',
SOUTH_AFRICA = 'ZA',
ZAMBIA = 'ZM',
ZIMBABWE = 'ZW',
}

View File

@ -0,0 +1 @@
export * from './countries-iso.enum';

View File

@ -0,0 +1 @@
export * from './numeric-to-iso.mapper';

View File

@ -0,0 +1,11 @@
import { CountriesNumericISO } from '../constants';
import { CountryIso } from '../enums';
// At module top-level
export const NumericToCountryIso: Record<string, CountryIso> = Object.entries(CountriesNumericISO).reduce(
(acc, [isoKey, numeric]) => {
acc[numeric] = isoKey as CountryIso;
return acc;
},
{} as Record<string, CountryIso>,
);

View File

@ -1,19 +1,23 @@
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { Cacheable } from 'cacheable';
@Injectable()
export class CacheService {
private readonly logger = new Logger(CacheService.name);
constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {}
get<T>(key: string): Promise<T | undefined> {
this.logger.log(`Getting value for key ${key}`);
return this.cache.get(key);
}
async set<T>(key: string, value: T, ttl?: number | string): Promise<void> {
this.logger.log(`Setting value for key ${key}`);
await this.cache.set(key, value, ttl);
}
async delete(key: string): Promise<void> {
this.logger.log(`Deleting value for key ${key}`);
await this.cache.delete(key);
}
}

View File

@ -1,23 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { DocumentType } from '~/document/enums';
import { DocumentService, OciService } from '~/document/services';
@Injectable()
export class LookupService {
private readonly logger = new Logger(LookupService.name);
constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {}
async findDefaultAvatar() {
this.logger.log(`Finding default avatar`);
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
await Promise.all(
documents.map(async (document) => {
document.url = await this.ociService.generatePreSignedUrl(document);
}),
);
this.logger.log(`Default avatar returned successfully`);
return documents;
}
async findDefaultTasksLogo() {
this.logger.log(`Finding default tasks logos`);
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO });
await Promise.all(
@ -26,6 +30,7 @@ export class LookupService {
}),
);
this.logger.log(`Default tasks logos returned successfully`);
return documents;
}
}

View File

@ -0,0 +1,38 @@
export const CARD_EMBOSSING_DETAILS_MOCK = {
ResponseHeader: {
Version: '1.0.0',
MsgUid: 'adaa1893-9f95-48a8-b7a1-0422bcf629b5',
Source: 'ZOD',
ServiceId: 'GetEmbossingInformation',
ReqDateTime: '2025-06-11T07:32:16.304Z',
RspDateTime: '2025-08-14T10:01:14.205',
ResponseCode: '000',
ResponseType: 'Success',
ProcessingTime: 67,
EncryptionKey: null,
ResponseDescription: 'Operation Successful',
LocalizedResponseDescription: null,
CustomerSpecificResponseDescriptionList: null,
HeaderUserDataList: null,
},
GetEmbossingInformationResponseDetails: {
icvv: '259',
Track1: '%B4017786818471184^AMMAR/QAFFAF^31102261029800997000000?',
Track2: ';4017786818471184=31102261029899700?',
Track3: null,
EmvTrack1: '1029800259000000',
EmvTrack2: '4017786818471184D31102261029825900',
ClearPan: '4017786818471184',
PinBlock: '663B1A71D8112D97',
Cvv: '997',
Cvv2: '834',
iCvv: '259',
Pvv: '0298',
EmbossingName: 'AMMAR QAFFAF',
ExpiryDate: '20311031',
OldPlasticExpiryDate: null,
CardStatus: '30',
OldPlasticCardStatus: ' ',
EmbossingRecord: null,
},
};

View File

@ -0,0 +1,750 @@
export const CREATE_APPLICATION_MOCK = {
ResponseHeader: {
Version: '1.0.0',
MsgUid: 'adaa1893-9f95-48a8-b7a1-0422bcf629b5',
Source: 'ZOD',
ServiceId: 'CreateNewApplication',
ReqDateTime: '2025-06-03T07:32:16.304Z',
RspDateTime: '2025-06-03T08:21:15.662',
ResponseCode: '000',
ResponseType: 'Success',
ProcessingTime: 1665,
EncryptionKey: null,
ResponseDescription: 'Operation Successful',
LocalizedResponseDescription: null,
CustomerSpecificResponseDescriptionList: null,
HeaderUserDataList: null,
},
CreateNewApplicationResponseDetails: {
InstitutionCode: '1100',
ApplicationTypeDetails: {
TypeCode: '01',
Description: 'Normal Primary',
Additional: false,
Corporate: false,
UserData: null,
},
ApplicationDetails: {
cif: null,
ApplicationNumber: '3300000000073',
ExternalApplicationNumber: '3',
ApplicationStatus: '04',
Organization: 0,
Product: '1101',
ApplicatonDate: '2025-05-29',
ApplicationSource: 'O',
SalesSource: null,
DeliveryMethod: 'V',
ProgramCode: null,
Campaign: null,
Plastic: null,
Design: null,
ProcessStage: '99',
ProcessStageStatus: 'S',
Score: null,
ExternalScore: null,
RequestedLimit: 0,
SuggestedLimit: null,
AssignedLimit: 0,
AllowedLimitList: null,
EligibilityCheckResult: '00',
EligibilityCheckDescription: null,
Title: 'Mr.',
FirstName: 'Abdalhamid',
SecondName: null,
ThirdName: null,
LastName: ' Ahmad',
FullName: 'Abdalhamid Ahmad',
EmbossName: 'ABDALHAMID AHMAD',
PlaceOfBirth: null,
DateOfBirth: '1999-01-07',
LocalizedDateOfBirth: '1999-01-07',
Age: 26,
Gender: 'M',
Married: 'U',
Nationality: '682',
IdType: '01',
IdNumber: '1089055972',
IdExpiryDate: '2031-09-17',
EducationLevel: null,
ProfessionCode: 0,
NumberOfDependents: 0,
EmployerName: 'N/A',
EmploymentYears: 0,
EmploymentMonths: 0,
EmployerPhoneArea: null,
EmployerPhoneNumber: null,
EmployerPhoneExtension: null,
EmployerMobile: null,
EmployerFaxArea: null,
EmployerFax: null,
EmployerCity: null,
EmployerAddress: null,
EmploymentActivity: null,
EmploymentStatus: null,
CIF: null,
BankAccountNumber: ' ',
Currency: {
CurrCode: '682',
AlphaCode: 'SAR',
},
RequestedCurrencyList: null,
CreditAccountNumber: '6000000000000000',
AccountType: '30',
OpenDate: null,
Income: 0,
AdditionalIncome: 0,
TotalIncome: 0,
CurrentBalance: 0,
AverageBalance: 0,
AssetsBalance: 0,
InsuranceBalance: 0,
DepositAmount: 0,
GuarenteeAccountNumber: null,
GuarenteeAmount: 0,
InstalmentAmount: 0,
AutoDebit: 'N',
PaymentMethod: '2',
BillingCycle: 'C1',
OldIssueDate: null,
OtherPaymentsDate: null,
MaximumDelinquency: null,
CreditBureauDecision: null,
CreditBureauUserData: null,
ECommerce: 'N',
NumberOfCards: 0,
OtherBank: null,
OtherBankDescription: null,
InsuranceProduct: null,
SocialCode: '000',
JobGrade: 0,
Flags: [
{
Position: 1,
Value: '0',
},
{
Position: 2,
Value: '0',
},
{
Position: 3,
Value: '0',
},
{
Position: 4,
Value: '0',
},
{
Position: 5,
Value: '0',
},
{
Position: 6,
Value: '0',
},
{
Position: 7,
Value: '0',
},
{
Position: 8,
Value: '0',
},
{
Position: 9,
Value: '0',
},
{
Position: 10,
Value: '0',
},
{
Position: 11,
Value: '0',
},
{
Position: 12,
Value: '0',
},
{
Position: 13,
Value: '0',
},
{
Position: 14,
Value: '0',
},
{
Position: 15,
Value: '0',
},
{
Position: 16,
Value: '0',
},
{
Position: 17,
Value: '0',
},
{
Position: 18,
Value: '0',
},
{
Position: 19,
Value: '0',
},
{
Position: 20,
Value: '0',
},
{
Position: 21,
Value: '0',
},
{
Position: 22,
Value: '0',
},
{
Position: 23,
Value: '0',
},
{
Position: 24,
Value: '0',
},
{
Position: 25,
Value: '0',
},
{
Position: 26,
Value: '0',
},
{
Position: 27,
Value: '0',
},
{
Position: 28,
Value: '0',
},
{
Position: 29,
Value: '0',
},
{
Position: 30,
Value: '0',
},
{
Position: 31,
Value: '0',
},
{
Position: 32,
Value: '0',
},
{
Position: 33,
Value: '0',
},
{
Position: 34,
Value: '0',
},
{
Position: 35,
Value: '0',
},
{
Position: 36,
Value: '0',
},
{
Position: 37,
Value: '0',
},
{
Position: 38,
Value: '0',
},
{
Position: 39,
Value: '0',
},
{
Position: 40,
Value: '0',
},
{
Position: 41,
Value: '0',
},
{
Position: 42,
Value: '0',
},
{
Position: 43,
Value: '0',
},
{
Position: 44,
Value: '0',
},
{
Position: 45,
Value: '0',
},
{
Position: 46,
Value: '0',
},
{
Position: 47,
Value: '0',
},
{
Position: 48,
Value: '0',
},
{
Position: 49,
Value: '0',
},
{
Position: 50,
Value: '0',
},
{
Position: 51,
Value: '0',
},
{
Position: 52,
Value: '0',
},
{
Position: 53,
Value: '0',
},
{
Position: 54,
Value: '0',
},
{
Position: 55,
Value: '0',
},
{
Position: 56,
Value: '0',
},
{
Position: 57,
Value: '0',
},
{
Position: 58,
Value: '0',
},
{
Position: 59,
Value: '0',
},
{
Position: 60,
Value: '0',
},
{
Position: 61,
Value: '0',
},
{
Position: 62,
Value: '0',
},
{
Position: 63,
Value: '0',
},
{
Position: 64,
Value: '0',
},
],
CheckFlags: [
{
Position: 1,
Value: '0',
},
{
Position: 2,
Value: '0',
},
{
Position: 3,
Value: '0',
},
{
Position: 4,
Value: '0',
},
{
Position: 5,
Value: '0',
},
{
Position: 6,
Value: '0',
},
{
Position: 7,
Value: '0',
},
{
Position: 8,
Value: '0',
},
{
Position: 9,
Value: '0',
},
{
Position: 10,
Value: '0',
},
{
Position: 11,
Value: '0',
},
{
Position: 12,
Value: '0',
},
{
Position: 13,
Value: '0',
},
{
Position: 14,
Value: '0',
},
{
Position: 15,
Value: '0',
},
{
Position: 16,
Value: '0',
},
{
Position: 17,
Value: '0',
},
{
Position: 18,
Value: '0',
},
{
Position: 19,
Value: '0',
},
{
Position: 20,
Value: '0',
},
{
Position: 21,
Value: '0',
},
{
Position: 22,
Value: '0',
},
{
Position: 23,
Value: '0',
},
{
Position: 24,
Value: '0',
},
{
Position: 25,
Value: '0',
},
{
Position: 26,
Value: '0',
},
{
Position: 27,
Value: '0',
},
{
Position: 28,
Value: '0',
},
{
Position: 29,
Value: '0',
},
{
Position: 30,
Value: '0',
},
{
Position: 31,
Value: '0',
},
{
Position: 32,
Value: '0',
},
{
Position: 33,
Value: '0',
},
{
Position: 34,
Value: '0',
},
{
Position: 35,
Value: '0',
},
{
Position: 36,
Value: '0',
},
{
Position: 37,
Value: '0',
},
{
Position: 38,
Value: '0',
},
{
Position: 39,
Value: '0',
},
{
Position: 40,
Value: '0',
},
{
Position: 41,
Value: '0',
},
{
Position: 42,
Value: '0',
},
{
Position: 43,
Value: '0',
},
{
Position: 44,
Value: '0',
},
{
Position: 45,
Value: '0',
},
{
Position: 46,
Value: '0',
},
{
Position: 47,
Value: '0',
},
{
Position: 48,
Value: '0',
},
{
Position: 49,
Value: '0',
},
{
Position: 50,
Value: '0',
},
{
Position: 51,
Value: '0',
},
{
Position: 52,
Value: '0',
},
{
Position: 53,
Value: '0',
},
{
Position: 54,
Value: '0',
},
{
Position: 55,
Value: '0',
},
{
Position: 56,
Value: '0',
},
{
Position: 57,
Value: '0',
},
{
Position: 58,
Value: '0',
},
{
Position: 59,
Value: '0',
},
{
Position: 60,
Value: '0',
},
{
Position: 61,
Value: '0',
},
{
Position: 62,
Value: '0',
},
{
Position: 63,
Value: '0',
},
{
Position: 64,
Value: '0',
},
],
Maker: null,
Checker: null,
ReferredTo: null,
ReferralReason: null,
UserData1: null,
UserData2: null,
UserData3: null,
UserData4: null,
UserData5: null,
AdditionalFields: [],
},
ApplicationStatusDetails: {
StatusCode: '04',
Description: 'Approved',
Canceled: false,
},
CorporateDetails: null,
CustomerDetails: {
Id: 115158,
CustomerCode: '100000024619',
IdNumber: ' ',
TypeId: 0,
PreferredLanguage: 'EN',
ExternalCustomerCode: null,
Title: ' ',
FirstName: ' ',
LastName: ' ',
DateOfBirth: null,
UserData1: '2031-09-17',
UserData2: '01',
UserData3: null,
UserData4: '682',
CustomerSegment: null,
Gender: 'U',
Married: 'U',
},
AccountDetailsList: [
{
Id: 21017,
InstitutionCode: '1100',
AccountNumber: '6899999999999999',
Currency: {
CurrCode: '682',
AlphaCode: 'SAR',
},
AccountTypeCode: '30',
ClassId: '2',
AccountStatus: '00',
VipFlag: '0',
BlockedAmount: 0,
EquivalentBlockedAmount: null,
UnclearCredit: 0,
EquivalentUnclearCredit: null,
AvailableBalance: 0,
EquivalentAvailableBalance: null,
AvailableBalanceToSpend: 0,
CreditLimit: 0,
RemainingCashLimit: null,
UserData1: 'D36407C9AE4C28D2185',
UserData2: null,
UserData3: 'D36407C9AE4C28D2185',
UserData4: null,
UserData5: 'SA2380900000752991120011',
},
],
CardDetailsList: [
{
pvv: null,
ResponseCardIdentifier: {
Id: 28595,
Pan: 'DDDDDDDDDDDDDDDDDDD',
MaskedPan: '999999_9999',
VPan: '1100000000000000',
Seqno: 0,
},
ExpiryDate: '2031-09-30',
EffectiveDate: '2025-06-02',
CardStatus: '30',
OldPlasticExpiryDate: null,
OldPlasticCardStatus: null,
EmbossingName: 'ABDALHAMID AHMAD',
Title: 'Mr.',
FirstName: 'Abdalhamid',
LastName: ' Ahmad',
Additional: false,
BatchNumber: 8849,
ServiceCode: '226',
Kinship: null,
DateOfBirth: '1999-01-07',
LastActivity: null,
LastStatusChangeDate: '2025-06-03',
ActivationDate: null,
DateLastIssued: null,
PVV: null,
UserData: '4',
UserData1: '3',
UserData2: null,
UserData3: null,
UserData4: null,
UserData5: null,
Memo: null,
CardAuthorizationParameters: null,
L10NTitle: null,
L10NFirstName: null,
L10NLastName: null,
PinStatus: '40',
OldPinStatus: '0',
CustomerIdNumber: '1089055972',
Language: 0,
},
],
},
};

View File

@ -0,0 +1,4 @@
export * from './card-embossing-details.mock';
export * from './create-application.mock';
export * from './initiate-kyc.mock';
export * from './inquire-application.mock';

View File

@ -0,0 +1,21 @@
export const INITIATE_KYC_MOCK = {
ResponseHeader: {
Version: '1.0.0',
MsgUid: 'f3a9d4b2-5c7a-4e2f-8121-9c4e5a6b7d8f',
Source: 'ZOD',
ServiceId: 'InitiateKyc',
ReqDateTime: '2025-08-07T14:20:00.000Z',
RspDateTime: '2025-08-07T14:20:00.123Z',
ResponseCode: '000',
ResponseType: 'Success',
ProcessingTime: 123,
ResponseDescription: 'KYC initiation successful',
},
InitiateKycResponseDetails: {
InstitutionCode: '1100',
TransId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
RandomNumber: '38',
Status: 'WAITING',
ExpiryDateTime: '2025-08-07T14:30:00.000Z',
},
};

View File

@ -0,0 +1,728 @@
export const INQUIRE_APPLICATION_MOCK = {
ResponseHeader: {
Version: '1.0.0',
MsgUid: 'adaa1893-9f95-48a8-b7a1-0422bcf629b4',
Source: 'ZOD',
ServiceId: 'InquireApplication',
ReqDateTime: '2023-07-18T10:34:12.553Z',
RspDateTime: '2025-06-03T11:14:54.748',
ResponseCode: '000',
ResponseType: 'Success',
ProcessingTime: 476,
EncryptionKey: null,
ResponseDescription: 'Operation Successful',
LocalizedResponseDescription: null,
CustomerSpecificResponseDescriptionList: null,
HeaderUserDataList: null,
},
InquireApplicationResponseDetails: {
InstitutionCode: '1100',
ApplicationTypeDetails: {
TypeCode: '01',
Description: 'Normal Primary',
Additional: false,
Corporate: false,
UserData: null,
},
ApplicationDetails: {
cif: null,
ApplicationNumber: '3300000000070',
ExternalApplicationNumber: '10000002',
ApplicationStatus: '04',
Organization: 0,
Product: '1101',
ApplicatonDate: '2025-05-29',
ApplicationSource: 'O',
SalesSource: null,
DeliveryMethod: 'V',
ProgramCode: null,
Campaign: null,
Plastic: null,
Design: null,
ProcessStage: '99',
ProcessStageStatus: 'S',
Score: null,
ExternalScore: null,
RequestedLimit: 0,
SuggestedLimit: null,
AssignedLimit: 0,
AllowedLimitList: null,
EligibilityCheckResult: '00',
EligibilityCheckDescription: null,
Title: 'Mr.',
FirstName: 'Abdalhamid',
SecondName: null,
ThirdName: null,
LastName: ' Ahmad',
FullName: 'Abdalhamid Ahmad',
EmbossName: 'ABDALHAMID AHMAD',
PlaceOfBirth: null,
DateOfBirth: '1999-01-07',
LocalizedDateOfBirth: '1999-01-07',
Age: 26,
Gender: 'M',
Married: 'U',
Nationality: '682',
IdType: '01',
IdNumber: '1089055972',
IdExpiryDate: '2031-09-17',
EducationLevel: null,
ProfessionCode: 0,
NumberOfDependents: 0,
EmployerName: 'N/A',
EmploymentYears: 0,
EmploymentMonths: 0,
EmployerPhoneArea: null,
EmployerPhoneNumber: null,
EmployerPhoneExtension: null,
EmployerMobile: null,
EmployerFaxArea: null,
EmployerFax: null,
EmployerCity: null,
EmployerAddress: null,
EmploymentActivity: null,
EmploymentStatus: null,
CIF: null,
BankAccountNumber: ' ',
Currency: {
CurrCode: '682',
AlphaCode: 'SAR',
},
RequestedCurrencyList: null,
CreditAccountNumber: '6823000000000019',
AccountType: '30',
OpenDate: null,
Income: 0,
AdditionalIncome: 0,
TotalIncome: 0,
CurrentBalance: 0,
AverageBalance: 0,
AssetsBalance: 0,
InsuranceBalance: 0,
DepositAmount: 0,
GuarenteeAccountNumber: null,
GuarenteeAmount: 0,
InstalmentAmount: 0,
AutoDebit: 'N',
PaymentMethod: '2',
BillingCycle: 'C1',
OldIssueDate: null,
OtherPaymentsDate: null,
MaximumDelinquency: null,
CreditBureauDecision: null,
CreditBureauUserData: null,
ECommerce: 'N',
NumberOfCards: 0,
OtherBank: null,
OtherBankDescription: null,
InsuranceProduct: null,
SocialCode: '000',
JobGrade: 0,
Flags: [
{
Position: 1,
Value: '0',
},
{
Position: 2,
Value: '0',
},
{
Position: 3,
Value: '0',
},
{
Position: 4,
Value: '0',
},
{
Position: 5,
Value: '0',
},
{
Position: 6,
Value: '0',
},
{
Position: 7,
Value: '0',
},
{
Position: 8,
Value: '0',
},
{
Position: 9,
Value: '0',
},
{
Position: 10,
Value: '0',
},
{
Position: 11,
Value: '0',
},
{
Position: 12,
Value: '0',
},
{
Position: 13,
Value: '0',
},
{
Position: 14,
Value: '0',
},
{
Position: 15,
Value: '0',
},
{
Position: 16,
Value: '0',
},
{
Position: 17,
Value: '0',
},
{
Position: 18,
Value: '0',
},
{
Position: 19,
Value: '0',
},
{
Position: 20,
Value: '0',
},
{
Position: 21,
Value: '0',
},
{
Position: 22,
Value: '0',
},
{
Position: 23,
Value: '0',
},
{
Position: 24,
Value: '0',
},
{
Position: 25,
Value: '0',
},
{
Position: 26,
Value: '0',
},
{
Position: 27,
Value: '0',
},
{
Position: 28,
Value: '0',
},
{
Position: 29,
Value: '0',
},
{
Position: 30,
Value: '0',
},
{
Position: 31,
Value: '0',
},
{
Position: 32,
Value: '0',
},
{
Position: 33,
Value: '0',
},
{
Position: 34,
Value: '0',
},
{
Position: 35,
Value: '0',
},
{
Position: 36,
Value: '0',
},
{
Position: 37,
Value: '0',
},
{
Position: 38,
Value: '0',
},
{
Position: 39,
Value: '0',
},
{
Position: 40,
Value: '0',
},
{
Position: 41,
Value: '0',
},
{
Position: 42,
Value: '0',
},
{
Position: 43,
Value: '0',
},
{
Position: 44,
Value: '0',
},
{
Position: 45,
Value: '0',
},
{
Position: 46,
Value: '0',
},
{
Position: 47,
Value: '0',
},
{
Position: 48,
Value: '0',
},
{
Position: 49,
Value: '0',
},
{
Position: 50,
Value: '0',
},
{
Position: 51,
Value: '0',
},
{
Position: 52,
Value: '0',
},
{
Position: 53,
Value: '0',
},
{
Position: 54,
Value: '0',
},
{
Position: 55,
Value: '0',
},
{
Position: 56,
Value: '0',
},
{
Position: 57,
Value: '0',
},
{
Position: 58,
Value: '0',
},
{
Position: 59,
Value: '0',
},
{
Position: 60,
Value: '0',
},
{
Position: 61,
Value: '0',
},
{
Position: 62,
Value: '0',
},
{
Position: 63,
Value: '0',
},
{
Position: 64,
Value: '0',
},
],
CheckFlags: [
{
Position: 1,
Value: '0',
},
{
Position: 2,
Value: '0',
},
{
Position: 3,
Value: '0',
},
{
Position: 4,
Value: '0',
},
{
Position: 5,
Value: '0',
},
{
Position: 6,
Value: '0',
},
{
Position: 7,
Value: '0',
},
{
Position: 8,
Value: '0',
},
{
Position: 9,
Value: '0',
},
{
Position: 10,
Value: '0',
},
{
Position: 11,
Value: '0',
},
{
Position: 12,
Value: '0',
},
{
Position: 13,
Value: '0',
},
{
Position: 14,
Value: '0',
},
{
Position: 15,
Value: '0',
},
{
Position: 16,
Value: '0',
},
{
Position: 17,
Value: '0',
},
{
Position: 18,
Value: '0',
},
{
Position: 19,
Value: '0',
},
{
Position: 20,
Value: '0',
},
{
Position: 21,
Value: '0',
},
{
Position: 22,
Value: '0',
},
{
Position: 23,
Value: '0',
},
{
Position: 24,
Value: '0',
},
{
Position: 25,
Value: '0',
},
{
Position: 26,
Value: '0',
},
{
Position: 27,
Value: '0',
},
{
Position: 28,
Value: '0',
},
{
Position: 29,
Value: '0',
},
{
Position: 30,
Value: '0',
},
{
Position: 31,
Value: '0',
},
{
Position: 32,
Value: '0',
},
{
Position: 33,
Value: '0',
},
{
Position: 34,
Value: '0',
},
{
Position: 35,
Value: '0',
},
{
Position: 36,
Value: '0',
},
{
Position: 37,
Value: '0',
},
{
Position: 38,
Value: '0',
},
{
Position: 39,
Value: '0',
},
{
Position: 40,
Value: '0',
},
{
Position: 41,
Value: '0',
},
{
Position: 42,
Value: '0',
},
{
Position: 43,
Value: '0',
},
{
Position: 44,
Value: '0',
},
{
Position: 45,
Value: '0',
},
{
Position: 46,
Value: '0',
},
{
Position: 47,
Value: '0',
},
{
Position: 48,
Value: '0',
},
{
Position: 49,
Value: '0',
},
{
Position: 50,
Value: '0',
},
{
Position: 51,
Value: '0',
},
{
Position: 52,
Value: '0',
},
{
Position: 53,
Value: '0',
},
{
Position: 54,
Value: '0',
},
{
Position: 55,
Value: '0',
},
{
Position: 56,
Value: '0',
},
{
Position: 57,
Value: '0',
},
{
Position: 58,
Value: '0',
},
{
Position: 59,
Value: '0',
},
{
Position: 60,
Value: '0',
},
{
Position: 61,
Value: '0',
},
{
Position: 62,
Value: '0',
},
{
Position: 63,
Value: '0',
},
{
Position: 64,
Value: '0',
},
],
Maker: null,
Checker: null,
ReferredTo: null,
ReferralReason: null,
UserData1: null,
UserData2: null,
UserData3: null,
UserData4: null,
UserData5: null,
AdditionalFields: [],
},
ApplicationStatusDetails: {
StatusCode: '04',
Description: 'Approved',
Canceled: false,
},
ApplicationHistoryList: null,
ApplicationAddressList: [
{
Id: 43859,
AddressLine1: '5536 abdullah Ibn al zubair ',
AddressLine2: ' Umm Alarad Dist.',
AddressLine3: null,
AddressLine4: null,
AddressLine5: null,
Directions: null,
City: 'AT TAIF',
PostalCode: null,
Province: null,
Territory: null,
State: null,
Region: null,
County: null,
Country: '682',
CountryDetails: {
IsoCode: '682',
Alpha3: 'SAU',
Alpha2: 'SA',
DefaultCurrency: {
CurrCode: '682',
AlphaCode: 'SAR',
},
Description: [
{
Language: 'EN',
Description: 'SAUDI ARABIA',
},
{
Language: 'GB',
Description: 'SAUDI ARABIA',
},
],
},
Phone1: '+966541884784',
Phone2: null,
Extension: null,
Email: 'a.ahmad@zod-alkhair.com',
Fax: null,
District: null,
PoBox: null,
OwnershipType: 'O',
UserData1: null,
UserData2: null,
AddressRole: 0,
AddressCustomValues: null,
},
],
CorporateDetails: null,
CustomerDetails: {
Id: 115129,
CustomerCode: '100000024552',
IdNumber: ' ',
TypeId: 0,
PreferredLanguage: 'EN',
ExternalCustomerCode: null,
Title: ' ',
FirstName: ' ',
LastName: ' ',
DateOfBirth: null,
UserData1: '2031-09-17',
UserData2: '01',
UserData3: null,
UserData4: '682',
CustomerSegment: null,
Gender: 'U',
Married: 'U',
},
BranchDetails: null,
CardAccountLinkageList: null,
},
};

View File

@ -0,0 +1,29 @@
export const getKycCallbackMock = (nationalId: string) => {
return {
InstId: '1100',
transId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
date: '20250807',
time: '150000',
status: 'SUCCESS',
firstName: 'John',
lastName: 'Doe',
dob: '19990107',
nationality: '682',
gender: 'M',
nationalIdExpiry: '20310917',
nationalId,
mobile: '+962798765432',
salaryMin: '500',
salaryMax: '1000',
incomeSource: 'Salary',
professionTitle: 'Software Engineer',
professionType: 'Full-Time',
isPep: 'N',
country: '682',
region: 'Mecca',
city: 'At-Taif',
neighborhood: 'Al-Hamra',
street: 'Al-Masjid Al-Haram',
building: '123',
};
};

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsString } from 'class-validator';
export class AccountCardStatusChangedWebhookRequest {
@ApiProperty()
@Expose({ name: 'InstId' })
@IsString()
instId!: string;
@ApiProperty()
@Expose({ name: 'cardId' })
@IsString()
cardId!: string;
@ApiProperty()
@Expose({ name: 'newStatus' })
@IsString()
newStatus!: string;
}

View File

@ -0,0 +1,68 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
import { IsNumber, IsPositive, IsString } from 'class-validator';
export class AccountTransactionWebhookRequest {
@Expose({ name: 'InstId' })
@IsString()
@ApiProperty({ name: 'InstId', example: '9000' })
instId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '143761' })
transactionId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '0037' })
transactionType!: string;
@Expose()
@IsString()
@ApiProperty({ example: '26' })
transactionCode!: string;
@Expose()
@IsString()
@ApiProperty({ example: '6823000000000018' })
accountNumber!: string;
@Expose()
@IsString()
@ApiProperty({ example: '6823000000000018' })
accountId!: string;
@Expose()
@Type(() => Number)
@IsNumber()
@ApiProperty({ example: '7080.15' })
otb!: number;
@Expose()
@Type(() => Number)
@IsNumber()
@IsPositive()
@ApiProperty({ example: '3050.95' })
amount!: number;
@Expose()
@IsString()
@ApiProperty({ example: 'C' })
sign!: string;
@Expose()
@IsString()
@ApiProperty({ example: '682' })
currency!: string;
@Expose()
@IsString()
@ApiProperty({ name: 'Date', example: '20241112' })
date!: string;
@Expose()
@IsString()
@ApiProperty({ name: 'Time', example: '125340' })
time!: string;
}

View File

@ -0,0 +1,158 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
import { IsNumber, IsString, Min, ValidateNested } from 'class-validator';
export class CardAcceptorLocationDto {
@Expose()
@IsString()
@ApiProperty()
merchantId!: string;
@Expose()
@IsString()
@ApiProperty()
merchantName!: string;
@Expose()
@IsString()
@ApiProperty()
merchantCountry!: string;
@Expose()
@IsString()
@ApiProperty()
merchantCity!: string;
@Expose()
@IsString()
@ApiProperty()
mcc!: string;
}
export class CardTransactionWebhookRequest {
@Expose({ name: 'InstId' })
@IsString()
@ApiProperty({ name: 'InstId', example: '1100' })
instId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '30829' })
cardId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '1234567890123456' })
transactionId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '277012*****3456' })
cardMaskedNumber!: string;
@Expose()
@IsString()
@ApiProperty({ example: '1234567890123456' })
accountNumber!: string;
@Expose({ name: 'Date' })
@IsString()
@ApiProperty({ name: 'Date', example: '20241112' })
date!: string;
@Expose({ name: 'Time' })
@IsString()
@ApiProperty({ name: 'Time', example: '125250' })
time!: string;
@Expose()
@Type(() => Number)
@IsNumber()
@ApiProperty({ example: '132' })
otb!: number;
@Expose()
@IsString()
@ApiProperty({ example: '0' })
transactionCode!: string;
@Expose()
@IsString()
@ApiProperty({ example: '1' })
messageClass!: string;
@Expose({ name: 'RRN' })
@IsString()
@ApiProperty({ name: 'RRN', example: '431712003306' })
rrn!: string;
@Expose()
@IsString()
@ApiProperty({ example: '3306' })
stan!: string;
@Expose()
@ValidateNested()
@Type(() => CardAcceptorLocationDto)
@ApiProperty({ type: CardAcceptorLocationDto })
cardAcceptorLocation!: CardAcceptorLocationDto;
@Type(() => Number)
@IsNumber()
@Min(0, { message: 'amount must be zero or a positive number' })
@ApiProperty({ example: '100.5' })
transactionAmount!: number;
@IsString()
@ApiProperty({ example: '682' })
transactionCurrency!: string;
@Type(() => Number)
@IsNumber()
@Min(0, { message: 'amount must be zero or a positive number' })
@ApiProperty({ example: '100.5' })
billingAmount!: number;
@IsString()
@ApiProperty({ example: '682' })
billingCurrency!: string;
@Type(() => Number)
@IsNumber()
@Min(0, { message: 'amount must be zero or a positive number' })
@ApiProperty({ example: '100.5' })
settlementAmount!: number;
@IsString()
@ApiProperty({ example: '682' })
settlementCurrency!: string;
@Expose()
@Type(() => Number)
@IsNumber()
@Min(0, { message: 'amount must be zero or a positive number' })
@ApiProperty({ example: '20' })
fees!: number;
@Expose()
@Type(() => Number)
@IsNumber()
@Min(0, { message: 'amount must be zero or a positive number' })
@ApiProperty({ example: '4.5' })
vatOnFees!: number;
@Expose()
@IsString()
@ApiProperty({ example: '9' })
posEntryMode!: string;
@Expose()
@IsString()
@ApiProperty({ example: '036657' })
authIdResponse!: string;
@Expose({ name: 'POSCDIM' })
@IsString()
@ApiProperty({ name: 'POSCDIM', example: '9' })
posCdim!: string;
}

Some files were not shown because too many files have changed in this diff Show More