Compare commits

...

689 Commits

Author SHA1 Message Date
eb072ef080 Merge pull request 'Added complete validation for expert claim submit state' (#286) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#286
2026-09-05 17:33:52 +03:30
SepehrYahyaee
e48dffef89 Added complete validation for expert claim submit state 2026-09-05 17:33:23 +03:30
4f7ee9a0de Merge pull request 'HOTFIX: not allowing empty price when expert tries to submit' (#285) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#285
2026-09-05 16:45:16 +03:30
SepehrYahyaee
feacad58ca HOTFIX: not allowing empty price when expert tries to submit 2026-09-05 16:44:40 +03:30
70fa49398d Merge pull request 'locations dynamically in fanavaran' (#284) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#284
2026-09-02 15:32:25 +03:30
5c2b660600 locations dynamically in fanavaran 2026-09-02 15:31:55 +03:30
ebfeef9e01 Merge pull request 'Fixed FileMaker and FileReviewer access to COMPLETED files' (#283) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#283
2026-09-01 21:58:43 +03:30
cf74d75146 Fixed FileMaker and FileReviewer access to COMPLETED files 2026-09-01 21:55:38 +03:30
f5a60eafb1 Merge pull request 'feat: complete in-person claims without final sign' (#282) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#282
2026-09-01 15:04:51 +03:30
SepehrYahyaee
ae49031c55 feat: complete in-person claims without final sign 2026-09-01 15:03:08 +03:30
23d636416d Merge pull request 'YARA-1259' (#281) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#281
2026-09-01 14:18:10 +03:30
SepehrYahyaee
6880de5960 YARA-1259 2026-09-01 14:13:52 +03:30
85bd892720 Merge pull request 'Fix PDF return data' (#280) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#280
2026-08-31 12:32:00 +03:30
SepehrYahyaee
a33466025d Fix PDF return data 2026-08-31 12:29:31 +03:30
a5d2f5a2b9 Merge pull request 'YARA-1241' (#279) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#279
2026-08-26 15:58:03 +03:30
SepehrYahyaee
226b63aefb Added new data for YARA-1241 2026-08-26 15:57:09 +03:30
5203c07154 merge upstream 2026-08-26 09:42:44 +03:30
SepehrYahyaee
6566b5f112 YARA-1246 2026-08-25 16:50:06 +03:30
24a38a6780 Merge pull request 'Added data for parsian to seed' (#278) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#278
2026-08-25 12:28:05 +03:30
SepehrYahyaee
dc3748ae94 Added data for parsian to seed 2026-08-25 12:27:21 +03:30
fe83c20e7e Merge pull request 'Added data seed script' (#277) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#277
2026-08-25 10:12:03 +03:30
SepehrYahyaee
e5a28238e5 Added data seed script 2026-08-25 10:11:19 +03:30
8cf7b7b229 Merge pull request 'YARA-1241' (#276) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#276
2026-08-24 16:51:16 +03:30
SepehrYahyaee
e4ef95a23e YARA-1241 2026-08-24 16:50:39 +03:30
5ec40f66dc Merge pull request 'YARA-985, YARA-1244' (#275) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#275
2026-08-24 16:34:27 +03:30
SepehrYahyaee
f6f4429ce7 YARA-985 2026-08-24 16:33:18 +03:30
SepehrYahyaee
2df7a889d3 YARA-1244 2026-08-24 11:11:31 +03:30
93edba412f Merge pull request 'Added case type for all GET APIs' (#274) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#274
2026-08-19 12:31:47 +03:30
SepehrYahyaee
3bd6bc6e9d Added case type for all GET APIs 2026-08-19 12:30:53 +03:30
SepehrYahyaee
26c80512ba Merge branch 'main' of git.ittalie.com:s.yahyaee/yara724-api 2026-08-19 10:22:06 +03:30
85fc83b564 merge upstream 2026-08-19 10:20:56 +03:30
SepehrYahyaee
f09f5b79f0 Added FA version of integrations documents 2026-08-18 11:24:06 +03:30
c7d77d30c4 Merge pull request 'fanavaran manuall test script + warn sms for fanavaran triggers at the last stage' (#273) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#273
2026-08-18 11:20:21 +03:30
693d602e49 sms template corrected and now fires at the end of the fanavaran stage (Expertise). Docs also added. 2026-08-18 11:18:33 +03:30
cb69b496b5 merge upstream 2026-08-18 10:15:33 +03:30
5942f51b4c Stop ignoring markdown under docs/ so Fanavaran docs can be committed without -f.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 17:41:22 +03:30
SepehrYahyaee
bfbcfcab37 Added documentation 2026-08-17 17:18:49 +03:30
0f9f702a00 Add Fanavaran-only flow-test script and document how to run it.
Track the tester and env template in git so a new tenant can be exercised without a YARA claim; keep filled client env files (secrets, national codes) ignored.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 17:05:33 +03:30
1d56326456 Merge pull request 'Added 'perfomedBy' field to the Timeline' (#272) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#272
2026-08-17 10:26:25 +03:30
SepehrYahyaee
d5aa9f3f1b Added 'perfomedBy' field to the Timeline 2026-08-17 10:26:02 +03:30
d8a2be091f Merge pull request 'YARA-1078' (#271) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#271
2026-08-16 12:37:26 +03:30
SepehrYahyaee
01f8a5b12c YARA-1078 2026-08-16 12:35:18 +03:30
e6ea5c2e8a Merge pull request 'Delegated the PDF creation task for FE, and BE now only returns the necessary data for it' (#270) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#270
2026-08-16 11:53:04 +03:30
SepehrYahyaee
875b52d761 Delegated the PDF creation task for FE, and BE now only returns the necessary data for it 2026-08-16 11:52:26 +03:30
f5aa25edf8 Merge pull request 'YARA-1169' (#269) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#269
2026-08-16 10:59:31 +03:30
SepehrYahyaee
85d7881b2b YARA-1169 2026-08-16 10:18:43 +03:30
7259eec949 Merge pull request 'Fixed VIN' (#268) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#268
2026-08-10 17:14:11 +03:30
SepehrYahyaee
42f4c6e8e3 Fixed VIN 2026-08-10 17:13:44 +03:30
71b3b1d786 Merge pull request 'Fixed vin inquiry' (#267) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#267
2026-08-10 16:51:40 +03:30
SepehrYahyaee
baac633443 Fixed vin inquiry 2026-08-10 16:51:00 +03:30
13231736d8 Merge pull request 'fix damaged parts' (#266) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#266
2026-08-10 14:23:02 +03:30
SepehrYahyaee
cbed681c8f fix damaged parts 2026-08-10 14:22:37 +03:30
3e1cde739f Merge pull request 'Fixed v4 damaged area part' (#265) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#265
2026-08-10 14:08:24 +03:30
SepehrYahyaee
70d7f34402 Fixed v4 damaged area part 2026-08-10 14:07:56 +03:30
f57b1b1171 Merge pull request 'Added vin inquiry for v6, fixed new damaged part for v4' (#264) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#264
2026-08-10 13:48:34 +03:30
SepehrYahyaee
6791c71809 Added vin inquiry for v6, fixed new damaged part for v4 2026-08-10 13:48:05 +03:30
6d955cd608 Merge pull request 'Fixed VIN placement for when the users use vin-based inquiry' (#263) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#263
2026-08-10 12:15:31 +03:30
SepehrYahyaee
ca7200e17d Fixed VIN placement for when the users use vin-based inquiry 2026-08-10 12:14:51 +03:30
60ed80fc87 Merge pull request 'Fixed YARA-1217' (#262) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#262
2026-08-10 10:35:14 +03:30
SepehrYahyaee
210e96fcf1 Fixed YARA-1217 2026-08-10 10:34:20 +03:30
63038f630d Merge pull request 'YARA-1218' (#261) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#261
2026-08-09 14:08:46 +03:30
SepehrYahyaee
8e4c794d61 YARA-1218 2026-08-09 14:08:11 +03:30
0663b35157 Merge pull request 'YARA-1217' (#260) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#260
2026-08-09 11:03:49 +03:30
SepehrYahyaee
d4cd8c9343 YARA-1217 2026-08-09 11:03:16 +03:30
2e8a8197a4 Merge pull request 'YARA-1216' (#259) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#259
2026-08-09 10:37:13 +03:30
SepehrYahyaee
3821bf36ef YARA-1216 2026-08-09 10:36:45 +03:30
7f131eb83e Merge pull request 'main' (#258) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#258
2026-08-09 10:24:49 +03:30
03ebc6649c merge upstream 2026-08-09 10:24:25 +03:30
448e5fb9ba lookup for driving licence type added + the new endpoint documented 2026-08-09 10:24:00 +03:30
f53ca43e54 Merge pull request 'YARA-1209, YARA-1214' (#257) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#257
2026-08-09 10:22:07 +03:30
SepehrYahyaee
473075e546 YARA-1209, YARA-1214 2026-08-09 10:21:12 +03:30
26da533a52 Merge pull request 'main' (#256) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#256
2026-08-08 15:59:50 +03:30
a03d557718 Update the ReadMe.md file, Documents of the apps should be LINKED here.
Suiggestion : 
for example we currently have [Fanavaran integration](docs/fanavaran/README.md) linked to the main files, I suggest for the flows and etc we should LINK the documented file.
2026-08-08 15:59:17 +03:30
5a4a511e84 fanavaran sales integration docs 2026-08-08 15:53:35 +03:30
91028f999c Merge pull request 'sms logging in mongo part was added' (#255) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#255
2026-08-08 12:15:33 +03:30
07d6cdbe7c sms logging in mongo part was added 2026-08-08 12:14:31 +03:30
e6038c73c5 Merge pull request 'licence number dummy data fills and never empty' (#254) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#254
2026-08-05 14:30:26 +03:30
d4487776fb licence number dummy data fills and never empty 2026-08-05 14:26:13 +03:30
07cd4776e9 Merge pull request 'Fixed car components' (#252) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#252
2026-08-03 19:01:01 +03:30
SepehrYahyaee
ab1e64c87b Fixed car components 2026-08-03 19:00:29 +03:30
dd6ee8ee20 Merge pull request 'Fix v5 + Fix ToDate field for damaged parts' (#251) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#251
2026-08-03 18:48:46 +03:30
SepehrYahyaee
cb23455bcd Fix v5 + Fix ToDate field for damaged parts 2026-08-03 18:48:15 +03:30
d528af5c1d Merge pull request 'Fixed rejection flow data on v5' (#250) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#250
2026-08-03 18:16:27 +03:30
SepehrYahyaee
793dc52640 Fixed rejection flow data on v5 2026-08-03 18:15:52 +03:30
01c1be40c7 Merge pull request 'fanavaran lookups done' (#249) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#249
2026-08-03 15:58:38 +03:30
738344a9d4 fanavaran lookups done 2026-08-03 15:53:36 +03:30
767317cce8 Merge pull request 'YARA-1202 FIX removed all damaged parts' (#248) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#248
2026-08-03 14:30:23 +03:30
SepehrYahyaee
65870c8d66 YARA-1202 FIX removed all damaged parts 2026-08-03 14:29:13 +03:30
70d05a7624 Merge pull request 'YARA-1202' (#247) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#247
2026-08-03 14:13:19 +03:30
SepehrYahyaee
192d4e72de YARA-1202 2026-08-03 14:12:30 +03:30
305a2965bf Merge pull request 'YARA-1201, YARA-1147' (#246) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#246
2026-08-03 12:55:47 +03:30
SepehrYahyaee
626b0ded34 YARA-1201, YARA-1147 2026-08-03 12:55:14 +03:30
c4f3558cda Merge pull request 'Fixed v6 statuses and steps' (#244) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#244
2026-08-03 12:13:16 +03:30
SepehrYahyaee
c8ccd943f2 Fixed v6 statuses and steps 2026-08-03 12:12:47 +03:30
e761c4b6b2 Merge pull request 'added offline inquiry in system setting also fix some bugs' (#243) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#243
2026-08-03 11:51:48 +03:30
7f672541ae added offline inquiry in system setting also fix some bugs 2026-08-03 11:49:24 +03:30
ab4f667c8d Merge pull request 'Fixed v6 flow' (#242) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#242
2026-08-03 11:06:35 +03:30
SepehrYahyaee
0b9bd59a66 Fixed v6 flow 2026-08-03 11:06:04 +03:30
fdfca80eb6 Merge pull request 'fanavaran configs seeding' (#240) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#240
2026-08-03 09:56:36 +03:30
f7b7cd13e8 fanavaran configs seeding 2026-08-03 09:55:30 +03:30
ca5c1900ff Merge pull request 'main' (#239) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#239
2026-08-02 17:42:49 +03:30
fc7488a204 merge upstream 2026-08-02 17:42:05 +03:30
298761233a expert id changed 2026-08-02 17:40:00 +03:30
05531260da Merge pull request 'main' (#238) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#238
2026-08-02 17:02:36 +03:30
b345818d43 fanavaran duplication request problems fixed. 2026-08-02 17:00:19 +03:30
c2f5c576fa merge upstream 2026-08-02 11:34:02 +03:30
8f66502c49 fanavaran rate bugs fixed + sms of claimId and claimNo added 2026-08-02 11:33:24 +03:30
6a26eaa6da Merge pull request 'mapping problems fixed' (#237) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#237
2026-08-01 17:57:41 +03:30
86f8b829fd mapping problems fixed 2026-08-01 17:56:58 +03:30
ea3db8f025 Merge pull request 'Added persian error messages (only for auth for now)' (#236) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#236
2026-07-31 20:01:14 +03:30
2cada1eba0 Added persian error messages (only for auth for now) 2026-07-31 20:00:46 +03:30
8448d2d771 Merge pull request 'YARA-1177' (#235) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#235
2026-07-31 19:50:03 +03:30
77cf420c33 YARA-1177 2026-07-31 19:48:59 +03:30
bf7e2ef97a Merge pull request 'mis spelling' (#234) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#234
2026-07-29 18:16:04 +03:30
426268fed2 mis spelling 2026-07-29 18:15:24 +03:30
a0874f4837 Merge pull request 'main' (#233) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#233
2026-07-29 17:34:37 +03:30
0c0c306740 merge upstream 2026-07-29 17:33:58 +03:30
914e97687f group lookups added also fanavaran module grant access 2026-07-29 17:32:57 +03:30
6b34b0cdf7 Merge pull request 'YARA-1182' (#232) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#232
2026-07-29 16:54:29 +03:30
SepehrYahyaee
e742d43201 YARA-1182 2026-07-29 16:53:36 +03:30
5e3da9dc02 Merge pull request 'Returning missing objects from v5' (#231) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#231
2026-07-29 14:23:44 +03:30
SepehrYahyaee
ea4fcbe713 Returning missing objects from v5 2026-07-29 14:23:13 +03:30
5eb50f94cc Merge pull request 'Fixed v5 rejection/approval flow' (#230) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#230
2026-07-29 10:53:11 +03:30
SepehrYahyaee
b0c7a0890c Fixed flow steps of v5 2026-07-29 10:51:42 +03:30
SepehrYahyaee
637cbb20d5 Fix v5 lock and re-submit mechanism 2026-07-29 10:16:18 +03:30
d72c811659 Merge pull request 'YARA-1181' (#229) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#229
2026-07-29 09:49:28 +03:30
SepehrYahyaee
45630a33ef YARA-1181 2026-07-29 09:49:01 +03:30
50ea476fa5 Merge pull request 'main' (#228) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#228
2026-07-28 16:59:02 +03:30
8609dbc377 merge upstream 2026-07-28 16:58:26 +03:30
0019a22ccd claim things fixed 2026-07-28 16:58:03 +03:30
8aae4b75e1 Merge pull request 'YARA-1135' (#227) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#227
2026-07-28 16:44:09 +03:30
SepehrYahyaee
891b1b2bbc YARA-1135 2026-07-28 16:43:29 +03:30
7ed102c6ef Merge pull request 'YARA-1148: add CALL_CENTER to LocalActorAuthGuard role allowlist' (#226) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#226
2026-07-28 15:44:00 +03:30
SepehrYahyaee
ec0d03fadf YARA-1148: add CALL_CENTER to LocalActorAuthGuard role allowlist 2026-07-28 15:40:11 +03:30
75adb2a3d2 Merge pull request 'main' (#225) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#225
2026-07-28 15:01:31 +03:30
SepehrYahyaee
4cf0461a48 YARA-1147 2026-07-28 15:00:09 +03:30
d7e6784746 Merge pull request 'main' (#1) from Yara724/api:main into main
Reviewed-on: s.yahyaee/yara724-api#1
2026-07-28 12:47:43 +03:30
d119cd64aa Merge pull request 'main' (#224) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#224
2026-07-28 12:25:22 +03:30
SepehrYahyaee
021d749962 Added permission for file maker and reviewers to access branches 2026-07-28 12:24:12 +03:30
SepehrYahyaee
61684156c6 YARA-1133 2026-07-27 14:00:54 +03:30
639e6ceebb Merge pull request 'YARA-1165' (#223) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#223
2026-07-27 11:46:36 +03:30
SepehrYahyaee
588a92c4b4 YARA-1165 2026-07-27 11:46:03 +03:30
f2e20b32eb Merge pull request 'YARA-1164' (#222) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#222
2026-07-27 11:39:39 +03:30
SepehrYahyaee
c94dd27a96 YARA-1164 2026-07-27 11:38:54 +03:30
6e1405c309 Merge pull request 'main' (#221) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#221
2026-07-27 10:39:57 +03:30
f053ee1348 merge upstream 2026-07-27 10:39:17 +03:30
182db56e15 the serial letter normalization added to the code to normalize the Heh , Ya , Kaf and etc to the correct character 2026-07-27 10:38:35 +03:30
9b348d567d Merge pull request 'YARA-1162' (#220) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#220
2026-07-27 10:16:22 +03:30
SepehrYahyaee
e4c3b7a16a YARA-1162 2026-07-27 10:15:46 +03:30
23fca04705 Merge pull request 'YARA-1154' (#219) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#219
2026-07-27 09:39:08 +03:30
SepehrYahyaee
4b9d946bfd YARA-1154 2026-07-27 09:31:07 +03:30
c85503e598 Merge pull request 'YARA-1136' (#218) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#218
2026-07-26 11:35:59 +03:30
SepehrYahyaee
9828aee8af YARA-1136 2026-07-26 11:35:21 +03:30
778544c321 Merge pull request 'YARA-1147' (#217) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#217
2026-07-25 12:32:22 +03:30
SepehrYahyaee
3afff67336 YARA-1147 2026-07-25 12:31:54 +03:30
793ff639ba Merge pull request 'Added insurer capabilities for super-admin' (#216) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#216
2026-07-25 11:55:42 +03:30
SepehrYahyaee
ec15cff557 Added insurer capabilities for super-admin 2026-07-25 11:55:00 +03:30
fd42adf9d6 Merge pull request 'Added inner parts to APIs for expert claim' (#215) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#215
2026-07-25 11:10:38 +03:30
SepehrYahyaee
4272790fad Added inner parts to APIs for expert claim 2026-07-25 11:10:02 +03:30
ac65cdb77b Merge pull request 'lookups and inquiries in lookups added' (#214) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#214
2026-07-25 11:04:42 +03:30
8430c68e3a lookups and inquiries in lookups added 2026-07-25 11:03:58 +03:30
49fe548215 Merge pull request 'Fixed v5 file maker approval field' (#213) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#213
2026-07-25 10:25:10 +03:30
SepehrYahyaee
777eae1028 Fixed v5 file maker approval field 2026-07-25 10:24:34 +03:30
b67dd733cd Merge pull request 'Fixed upload documents counting for v4' (#212) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#212
2026-07-25 09:53:07 +03:30
SepehrYahyaee
4818f73252 Fixed upload documents counting for v4 2026-07-25 09:52:42 +03:30
cc8a5354c7 Merge pull request 'v4 bug fixed' (#211) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#211
2026-07-25 09:29:42 +03:30
SepehrYahyaee
2d5ade33d2 v4 bug fixed 2026-07-25 09:29:12 +03:30
b73c92c21f Merge pull request 'Fixed Bugs: 1) V4 incorrect advancement to file maker approval state, 2) V4 race condition for finishing file maker steps' (#210) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#210
2026-07-23 13:44:05 +03:30
f9f462d47b Fixed Bugs: 1) V4 incorrect advancement to file maker approval state, 2) V4 race condition for finishing file maker steps 2026-07-23 13:43:37 +03:30
c3eb36dc41 Merge pull request 'Fixed v4 sign' (#209) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#209
2026-07-22 17:37:00 +03:30
SepehrYahyaee
f75ecf5c2c Fixed v4 sign 2026-07-22 17:36:29 +03:30
75c4cb6a05 Merge pull request 'Fixed v4/v5 flow' (#208) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#208
2026-07-22 17:22:55 +03:30
SepehrYahyaee
7a4277f8b2 Fixed v4/v5 flow 2026-07-22 17:22:27 +03:30
e50bc78344 Merge pull request 'Fixed sign' (#207) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#207
2026-07-22 15:47:02 +03:30
SepehrYahyaee
2c1cd93dd0 Fixed sign 2026-07-22 15:46:33 +03:30
ffd44df718 Merge pull request 'Fix v2 flow sign of second party' (#206) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#206
2026-07-22 15:35:53 +03:30
SepehrYahyaee
64865b70f2 Fix v2 flow sign of second party 2026-07-22 15:35:14 +03:30
c207c30be9 Merge pull request 'Fixed v2 flow' (#205) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#205
2026-07-22 15:10:00 +03:30
SepehrYahyaee
fcb169e9ac Fixed v2 flow 2026-07-22 15:09:34 +03:30
1867292499 Merge pull request 'Fixed v2 flow' (#204) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#204
2026-07-22 14:53:33 +03:30
SepehrYahyaee
2cc96f6132 Fixed v2 flow 2026-07-22 14:52:51 +03:30
4d5b91d4fe Merge pull request 'update the fanavaran for both tejarat no and parsian clients , dont forget about the env files' (#203) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#203
2026-07-20 16:30:15 +03:30
8ba97537a4 update the fanavaran for both tejarat no and parsian clients , dont forget about the env files 2026-07-20 16:28:25 +03:30
70160543a2 Merge pull request 'Fixed v2 mirror' (#202) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#202
2026-07-20 11:45:26 +03:30
SepehrYahyaee
8460c86820 Fixed v2 mirror 2026-07-20 11:44:50 +03:30
61f4181065 Merge pull request 'BUG fix of status' (#201) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#201
2026-07-19 16:51:15 +03:30
SepehrYahyaee
4058cb4a61 BUG fix of status 2026-07-19 16:50:45 +03:30
b2ad43d050 Merge pull request 'YARA-1020' (#200) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#200
2026-07-19 16:35:15 +03:30
SepehrYahyaee
9fc4ad9931 YARA-1020 2026-07-19 16:34:44 +03:30
213cdd765b Merge pull request 'YARA-985' (#199) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#199
2026-07-19 11:47:12 +03:30
SepehrYahyaee
06d69aa4d0 YARA-985 2026-07-19 11:44:15 +03:30
318a5c74dd Merge pull request 'removed guard' (#198) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#198
2026-07-18 16:14:59 +03:30
SepehrYahyaee
7241ae3270 removed guard 2026-07-18 16:14:27 +03:30
1f4a520145 Merge pull request 'Removed the guard of accidentWay' (#197) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#197
2026-07-18 16:03:55 +03:30
SepehrYahyaee
59a1b9064e Removed the guard of accidentWay 2026-07-18 16:03:32 +03:30
0420eda35f Merge pull request 'Edited 2 APIs names' (#196) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#196
2026-07-18 15:28:27 +03:30
SepehrYahyaee
482d2b01f1 Edited API route 2026-07-18 15:27:23 +03:30
SepehrYahyaee
04d7966776 Changed API name of v2 blame mirror 2026-07-18 15:25:59 +03:30
b129c1ef9b Merge pull request 'YARA-1061, Fixed v3 mirror flow' (#195) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#195
2026-07-18 15:02:18 +03:30
SepehrYahyaee
a1fca82cb2 Fixed v3 mirror flow 2026-07-18 15:01:13 +03:30
SepehrYahyaee
5b114c2069 YARA-1061 2026-07-18 14:30:26 +03:30
5e4897f609 Fix empty Rocket.Chat notify (Woodpecker ${} escaping) 2026-07-15 17:06:27 +03:30
a79c3ca05f Fix git pull SSH in pipeline (host keys + known_hosts) 2026-07-15 16:59:46 +03:30
36fa1c552e Allow git in bind-mounted workspace (safe.directory) 2026-07-15 16:43:31 +03:30
9742fecc11 Update .woodpecker.yml 2026-07-15 16:35:40 +03:30
8007c30efd Fix path typo of workspace 2026-07-15 16:26:06 +03:30
144f8f3e2a Update .woodpecker.yml docker repositories 2026-07-15 16:05:01 +03:30
8674dd8762 Merge pull request 'Fixed v4/v5 car capture flow' (#194) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#194
2026-07-15 16:00:01 +03:30
SepehrYahyaee
f39c50aeb0 Fixed v4/v5 car capture flow 2026-07-15 15:59:21 +03:30
2fda740171 Update .woodpecker.yml 2026-07-15 15:54:28 +03:30
72e5bd616c Update .woodpecker.yml 2026-07-15 15:51:29 +03:30
4b41a60f64 Add .woodpecker.yml 2026-07-15 15:47:53 +03:30
9310285bd4 Merge pull request 'FIX v5 flow' (#193) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#193
2026-07-15 14:57:38 +03:30
SepehrYahyaee
2a8b66bc16 FIX v5 flow 2026-07-15 14:56:58 +03:30
9168a6bdcd Merge pull request 'Added fonts for PDF' (#192) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#192
2026-07-15 13:24:17 +03:30
SepehrYahyaee
057bedeb0c Added fonts for PDF 2026-07-15 13:23:43 +03:30
808a3b8526 Merge pull request 'YARA-994 and fixed metal plate bug' (#191) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#191
2026-07-15 10:34:34 +03:30
SepehrYahyaee
da7a4f8890 YARA-994 and fixed metal plate bug 2026-07-15 10:33:50 +03:30
21e55012be Merge pull request 'Fixed file maker retrieving files' (#190) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#190
2026-07-14 14:31:20 +03:30
SepehrYahyaee
385757c3a0 Fixed file maker retrieving files 2026-07-14 14:30:41 +03:30
a1b122a33b Merge pull request 'Fixed file maker view files error' (#189) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#189
2026-07-14 13:53:02 +03:30
SepehrYahyaee
aec9e76918 Fixed file maker view files error 2026-07-14 13:52:12 +03:30
7c59c2407e Merge pull request 'YARA-1115, YARA-1117, YARA-1119' (#188) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#188
2026-07-14 12:07:20 +03:30
SepehrYahyaee
168e52a475 YARA-1117 and fixed completed status after in person visit has been called 2026-07-14 11:51:52 +03:30
SepehrYahyaee
4aa6e03afb YARA-1119 2026-07-14 11:32:03 +03:30
SepehrYahyaee
36a34e27b3 YARA-1115 2026-07-14 11:23:39 +03:30
SepehrYahyaee
6387ebaed0 Fixed a bug where file makers would be able to view v4 files as well as v5 ones 2026-07-14 10:19:30 +03:30
22a5990934 Merge pull request 'Fixed blame status after v4/v5 flows gets completed' (#187) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#187
2026-07-14 10:08:32 +03:30
SepehrYahyaee
e5de99adde Fixed blame status after v4/v5 flows gets completed 2026-07-14 10:07:00 +03:30
c7fd2a6b33 Merge pull request 'YARA-1110' (#186) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#186
2026-07-13 11:54:28 +03:30
SepehrYahyaee
5595083e86 YARA-1110 2026-07-13 11:53:47 +03:30
2296fa5d86 Merge pull request 'YARA-1094, YARA-1095, YARA-1096' (#185) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#185
2026-07-12 14:08:38 +03:30
SepehrYahyaee
72dec7a917 YARA-1094, YARA-1095, YARA-1096 2026-07-12 14:07:27 +03:30
67019851de Merge pull request 'YARA-982, YARA-1062, YARA-1069' (#184) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#184
2026-07-12 11:45:49 +03:30
SepehrYahyaee
c955deda5c YARA-1069 2026-07-12 11:44:32 +03:30
SepehrYahyaee
9b83db882b YARA-982 2026-07-12 11:27:58 +03:30
SepehrYahyaee
bced6a0ec7 YARA-1062 2026-07-12 11:11:50 +03:30
5d1110b6e9 Merge pull request 'YARA-947, YARA-986, YARA-1038' (#183) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#183
2026-07-11 17:52:42 +03:30
SepehrYahyaee
a7fe04c032 YARA-986 2026-07-11 17:51:14 +03:30
SepehrYahyaee
0dcb2cf2ca YARA-947, YARA-1038 2026-07-11 15:34:01 +03:30
8b125af4e7 Merge pull request 'YARA-914, YARA-917, YARA-923, YARA-937, YARA-957, YARA-1056, YARA-1061' (#182) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#182
2026-07-11 13:20:00 +03:30
1559a40213 merge upstream 2026-07-11 13:17:55 +03:30
SepehrYahyaee
54ae82aa38 YARA-1056 2026-07-11 13:16:14 +03:30
SepehrYahyaee
7a3ddcc7be YARA-937 2026-07-11 12:23:00 +03:30
SepehrYahyaee
da3f57870e YARA-917 2026-07-11 12:00:11 +03:30
ac7ee941b8 Merge pull request 'main' (#181) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#181
2026-07-11 11:40:45 +03:30
5d005d5eee env example 2026-07-11 11:39:52 +03:30
b65d9bfe81 driverId problem fixed , also added a captcha required env called : CAPTCHA_ENABLED= to disable or enable CAPTCHA in development 2026-07-11 11:39:36 +03:30
SepehrYahyaee
04f51167c2 YARA-1061 2026-07-11 11:38:49 +03:30
SepehrYahyaee
2c8fd3960f YARA-957 2026-07-11 11:25:42 +03:30
SepehrYahyaee
e59058520c YARA-914, YARA-923 2026-07-11 11:16:16 +03:30
80122e7772 Merge pull request 'main' (#180) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#180
2026-07-07 18:53:35 +03:30
f409d78ede merge upstream 2026-07-07 18:53:01 +03:30
Soheil Hajizadeh
24340eb810 claim request management change 2026-07-07 18:52:07 +03:30
41d1de77eb Merge pull request 'main' (#179) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#179
2026-07-07 17:30:48 +03:30
0aadc64cd3 merge upstream 2026-07-07 17:30:23 +03:30
Soheil Hajizadeh
1e6c36cbd4 lookup of person role added + inquiry by unique identifier + put driver id in payload 2026-07-07 17:29:48 +03:30
ae23a10d33 Merge pull request 'main' (#178) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#178
2026-07-07 13:47:07 +03:30
1c6678d8e4 merge upstream 2026-07-07 13:46:27 +03:30
Soheil Hajizadeh
ad5ff28be4 fanavaran damage case updated 2026-07-07 13:42:06 +03:30
1fe2c77c70 Merge pull request 'main' (#177) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#177
2026-07-05 15:49:53 +03:30
45a51f2c24 merge upstream 2026-07-05 15:47:59 +03:30
Soheil Hajizadeh
0514548136 policyCINumber added to the payload 2026-07-05 15:45:39 +03:30
5b4cc0560f Merge pull request 'V4: add WAITING_FOR_FILE_REVIEWER claim status; fix select-outer-parts for split flow' (#176) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#176
2026-07-05 15:16:43 +03:30
e9c02811f7 V4: add WAITING_FOR_FILE_REVIEWER claim status; fix select-outer-parts for split flow
- Add ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER (V4 split flow only)
- Set claim status to WAITING_FOR_FILE_REVIEWER when FileMaker uploads
  the last required document (v3InPersonFlow path), replacing the old
  behaviour that incorrectly auto-advanced to SELECT_OUTER_PARTS
- advanceV3ClaimToOuterPartsIfReady: also allow canAdvance when
  claimCase.status === WAITING_FOR_FILE_REVIEWER so the FileReviewer
  can call select-outer-parts after submitting accident fields
- Add WAITING_FOR_FILE_REVIEWER to CLAIM_USER_PHASE (unified-file-status)
  so the file still resolves to IN_PROGRESS in the unified status report
- Add WAITING_FOR_FILE_REVIEWER to CLAIM_IN_PROGRESS_STATUSES
  (expert-panel-status-report) for the expert report bucket
- Add WAITING_FOR_FILE_REVIEWER to claimInHandling set
  (expert-insurer.service) so insurer stats count these correctly
2026-07-05 15:13:19 +03:30
8ab49c9a35 Merge pull request 'Fixed reviewer flow' (#175) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#175
2026-07-05 11:47:11 +03:30
f0cd4461a8 Fixed reviewer flow 2026-07-05 11:46:37 +03:30
3205a89611 Merge pull request 'Fixed GET APIs for FileMaker and FileReviewer getting their own files' (#174) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#174
2026-07-05 11:35:48 +03:30
14bc075521 Fixed GET APIs for FileMaker and FileReviewer getting their own files 2026-07-05 11:34:51 +03:30
1fe66b2d91 Merge pull request 'Adding file makers and file reviewers to global roles' (#173) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#173
2026-07-04 14:21:17 +03:30
033a853b51 Adding file makers and file reviewers to global roles 2026-07-04 12:54:47 +03:30
f05891a112 Merge pull request 'Fixed outer parts flow bug in v4' (#172) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#172
2026-07-04 10:29:10 +03:30
7641c56440 Fixed outer parts flow bug in v4 2026-07-04 10:28:20 +03:30
ae83100ef4 Merge pull request 'Added roles for GET car parts APIs' (#171) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#171
2026-07-02 13:17:37 +03:30
9e2cf9bcdf Added roles for GET car parts APIs 2026-07-02 13:17:04 +03:30
ced08fc1f7 Merge pull request 'fix it' (#170) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#170
2026-07-01 18:06:07 +03:30
d525b8dd0d fix it 2026-07-01 18:05:09 +03:30
b43f1a86dc Merge pull request 'Added blame Id for claims' (#169) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#169
2026-07-01 17:45:21 +03:30
SepehrYahyaee
5df39b502e Added blame Id for claims 2026-07-01 17:44:44 +03:30
49564cc1c6 Merge pull request 'Fixed statuses' (#168) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#168
2026-07-01 17:22:34 +03:30
SepehrYahyaee
29939eee20 Fixed statuses 2026-07-01 17:21:54 +03:30
ae789323d8 Merge pull request 'FIxed file reviewer bugs' (#167) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#167
2026-07-01 16:56:03 +03:30
SepehrYahyaee
6be9ff16e1 FIXED FILE REVIEWER GET ALL 2026-07-01 16:54:24 +03:30
SepehrYahyaee
0c5a2fe38b Fixed accident-details bug 2026-07-01 15:58:45 +03:30
5ef8310cb0 Merge pull request 'main' (#166) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#166
2026-07-01 15:14:56 +03:30
d2cb9444f3 merge upstream 2026-07-01 15:13:52 +03:30
67471fb9ce fanavaran stage by stage implemented i am so bored to send smart commit sorry MR sina 2026-07-01 15:13:22 +03:30
569e7592ec Merge pull request 'YARA-1025, YARA-1058, YARA-1075, YARA-1076' (#165) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#165
2026-07-01 12:25:07 +03:30
SepehrYahyaee
e2a9232523 YARA-1058 2026-07-01 12:23:34 +03:30
SepehrYahyaee
dc006735ba Duplicated capture-requirements endpoint for file-maker 2026-07-01 12:15:22 +03:30
SepehrYahyaee
f92cee0575 YARA-1075 2026-07-01 12:11:19 +03:30
SepehrYahyaee
493be68b80 YARA-1025 2026-07-01 12:04:39 +03:30
SepehrYahyaee
edf027acd3 YARA-1076 2026-07-01 12:00:29 +03:30
0698338d4c Merge pull request 'Access new roles' (#164) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#164
2026-07-01 11:11:55 +03:30
SepehrYahyaee
f3c7f6a7e0 Access new roles 2026-07-01 11:11:27 +03:30
607472cd89 Merge pull request 'Added fileMaker and fileReviewer for new flow' (#163) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#163
2026-06-30 13:54:59 +03:30
SepehrYahyaee
65e7476642 Added fileMaker and fileReviewer for new flow 2026-06-30 13:54:21 +03:30
9068765c25 Merge pull request 'main' (#162) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#162
2026-06-30 11:52:40 +03:30
99c819caeb feat(fanavaran): add auth token script
Why:
- Manual curl token setup was error-prone and allowed stale appToken reuse.

Changes:
- Add a script that accepts tejaratno or parsian, calls GetAppToken, then Login.
- Document the script flow and fill known curl variables from the codebase.

Impact:
- Users can generate fresh Fanavaran tokens without manually copying multi-step curl commands.
2026-06-30 11:51:40 +03:30
8d396762a2 fix(fanavaran): select latest active policy by end date
Why:
- Fanavaran policy inquiry response order is inconsistent, so selecting the last item can choose an old or expired policy.

Changes:
- Select the policy with the latest Jalali EndDate.
- Reject empty policy responses, expired latest policies, and latest policies without a valid PolicyId.
- Stop Fanavaran submission when PolicyId cannot be resolved.

Impact:
- Fanavaran submit now fails clearly instead of continuing with PolicyId: null.
2026-06-30 11:51:11 +03:30
f3686575ca Merge pull request 'Fix CAR_GREEN_CARD upload error' (#161) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#161
2026-06-28 16:59:44 +03:30
SepehrYahyaee
b9fe1bfd52 Fix CAR_GREEN_CARD upload error 2026-06-28 16:58:53 +03:30
6eca1f5dad Merge pull request 'YARA-1061, YARA-1072, YARA-1073, YARA-1074' (#160) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#160
2026-06-28 16:05:50 +03:30
SepehrYahyaee
6477778835 YARA-1074 2026-06-28 16:04:25 +03:30
SepehrYahyaee
4552450fbc YARA-1073 2026-06-28 15:53:34 +03:30
SepehrYahyaee
f7f7f4548d YARA-1061 2026-06-28 15:40:35 +03:30
SepehrYahyaee
220b39ea5a YARA-1072 2026-06-28 15:04:18 +03:30
4a045f564c Merge pull request 'Fix CAR_GREEN_CARD place to upload for v3' (#159) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#159
2026-06-28 14:22:10 +03:30
SepehrYahyaee
d3249e9854 Fix CAR_GREEN_CARD place to upload for v3 2026-06-28 14:21:08 +03:30
7e597db423 Merge pull request 'logged vehicle usage code' (#158) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#158
2026-06-27 17:51:29 +03:30
8303bf4467 logged vehicle usage code 2026-06-27 17:49:59 +03:30
ebe8671bd3 Merge pull request 'main' (#157) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#157
2026-06-27 16:51:30 +03:30
5022178569 merge upstream 2026-06-27 16:51:10 +03:30
2fbf40ef19 log the my policies 2026-06-27 16:50:57 +03:30
dca9e1f8d4 Merge pull request 'PDF generation + mismatched data fixes' (#156) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#156
2026-06-27 16:44:25 +03:30
SepehrYahyaee
8f8ed8a94e YARA-1062 2026-06-27 14:44:01 +03:30
SepehrYahyaee
3c7a656cd7 Fixed mismatched insurance number getting saved 2026-06-27 13:04:22 +03:30
87ece0d89d Merge pull request 'main' (#155) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#155
2026-06-27 10:57:23 +03:30
7af3f3627a merge upstream 2026-06-27 10:53:49 +03:30
7e1ae328ac updated the accident cause id to default value 6 2026-06-27 10:53:10 +03:30
094d9048ba merge upstream 2026-06-27 09:30:55 +03:30
9afb6a8a6e Merge pull request 'main' (#154) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#154
2026-06-23 18:24:40 +03:30
2df25e26bb merge upstream 2026-06-23 18:24:15 +03:30
c7fb6174a0 lookups update per client 2026-06-23 18:23:09 +03:30
136b22fd81 Merge pull request 'main' (#153) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#153
2026-06-23 17:44:03 +03:30
16cdf2e7b0 merge upstream 2026-06-23 17:43:42 +03:30
488c9180af address hardcoded 2026-06-23 17:43:24 +03:30
75c556a013 Merge pull request 'main' (#152) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#152
2026-06-23 16:00:15 +03:30
0f53bfabf5 merge upstream 2026-06-23 15:59:21 +03:30
8bf3cfe5e9 audit fanavaran logs and missing fields null fixed 2026-06-23 15:58:49 +03:30
SepehrYahyaee
523172da67 PDF generation packages 2026-06-23 15:56:51 +03:30
SepehrYahyaee
2fc6015213 PDF generation 2026-06-23 15:56:19 +03:30
f6ec7644ff Merge pull request 'update the fanavaran' (#151) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#151
2026-06-23 13:59:43 +03:30
bc70a8c930 update the fanavaran 2026-06-23 13:57:16 +03:30
SepehrYahyaee
cca3ed01a4 YARA-1045 2026-06-23 13:31:04 +03:30
4caa958175 Merge pull request 'main' (#150) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#150
2026-06-23 13:20:58 +03:30
7d10c00ce5 merge upstream 2026-06-23 13:17:44 +03:30
114e5e6604 fanavaran added 2 apis for get payload and submit manually 2026-06-23 13:17:30 +03:30
f00cb226a7 Merge pull request 'Fixed workflow step' (#149) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#149
2026-06-23 11:04:03 +03:30
SepehrYahyaee
e2b879d943 Fixed workflow step 2026-06-23 11:03:06 +03:30
d84bd24682 Merge pull request 'FAKE SMS + SEARCH APIs' (#148) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#148
2026-06-23 10:28:01 +03:30
SepehrYahyaee
ed2b6948cf FAKE SMS 2026-06-23 10:26:51 +03:30
SepehrYahyaee
c6b417ced7 Fix searching on GET APIs 2026-06-23 10:22:46 +03:30
16d3c54613 Merge pull request 'Fix v3 mirror flow' (#147) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#147
2026-06-22 17:31:32 +03:30
SepehrYahyaee
db569db9d1 Fix v3 mirror flow 2026-06-22 17:30:58 +03:30
83e82ea68e Merge pull request 'Added v3 of field-expert' (#146) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#146
2026-06-22 13:06:21 +03:30
SepehrYahyaee
8f29bb564c Added v3 of field-expert 2026-06-22 13:05:11 +03:30
89e715b0c9 Merge pull request 'update the car name' (#145) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#145
2026-06-21 17:45:51 +03:30
44f7ce5b54 update the car name 2026-06-21 17:44:18 +03:30
a2396da9e4 Merge pull request 'main' (#144) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#144
2026-06-21 17:13:11 +03:30
64f6245e06 merge upstream 2026-06-21 17:12:43 +03:30
df79a4d307 upserting the mongo error 2026-06-21 17:11:50 +03:30
65c30a6cba Merge pull request 'inquiry refresh service + fanavaran client config' (#143) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#143
2026-06-21 16:23:24 +03:30
0dd6c8ff78 inquiry refresh service + fanavaran client config 2026-06-21 16:20:03 +03:30
0442d04f20 Merge pull request 'Fixed smsApiKey index error and err handling in ESG' (#142) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#142
2026-06-21 12:19:15 +03:30
SepehrYahyaee
d0e7694374 Fixed smsApiKey index error and err handling in ESG 2026-06-21 12:18:08 +03:30
570fa865de Merge pull request 'Fix expert codes' (#141) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#141
2026-06-20 16:36:41 +03:30
SepehrYahyaee
8741d2ba82 Fix expert codes 2026-06-20 16:30:57 +03:30
7b53c98791 Merge pull request 'Fixed mock data' (#140) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#140
2026-06-20 15:49:27 +03:30
SepehrYahyaee
59eddb8e0e Fixed mock data 2026-06-20 15:48:39 +03:30
4e20fc5c96 Merge pull request 'YARA-1034, YARA-1035' (#139) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#139
2026-06-20 14:52:23 +03:30
SepehrYahyaee
4fabed77e5 YARA-1035 2026-06-20 14:51:01 +03:30
SepehrYahyaee
2e4b10455b YARA-1034 2026-06-20 14:30:29 +03:30
1bbf0de960 Merge pull request 'Added common utils' (#138) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#138
2026-06-20 12:05:15 +03:30
SepehrYahyaee
15fcb011aa Added common utils 2026-06-20 12:04:51 +03:30
2c52c14e03 Merge pull request 'Fixed mismatched userId on field expert for claim' (#137) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#137
2026-06-20 11:55:34 +03:30
SepehrYahyaee
5a89a0ff16 Fixed mismatched userId on field expert for claim 2026-06-20 11:53:18 +03:30
d355771518 Merge pull request 'ServeRoot fixing on WORKDIR' (#136) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#136
2026-06-19 15:06:59 +03:30
f4301428c7 ServeRoot fixing on WORKDIR 2026-06-19 15:05:35 +03:30
e3406f7645 Merge pull request 'Inquiry fix' (#135) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#135
2026-06-19 14:26:42 +03:30
4d6183fa24 Inquiry fix 2026-06-19 14:24:57 +03:30
ce4945ebb8 Merge pull request 'Error handling on empty damaged parts' (#134) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#134
2026-06-19 13:41:21 +03:30
5cada3c6c8 Error handling on empty damaged parts 2026-06-19 13:40:38 +03:30
761f0cb679 Merge pull request 'Added field expert support for insurer' (#133) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#133
2026-06-19 10:53:08 +03:30
3c61e4397a Added field expert support for insurer 2026-06-19 10:52:47 +03:30
fae7e9b13b Merge pull request 'Fixed users not being able to view their files' (#132) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#132
2026-06-19 09:51:16 +03:30
3c863bb90c Fixed users not being able to view their files 2026-06-19 09:50:42 +03:30
0c5756d325 Merge pull request 'Fixed GET users' (#131) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#131
2026-06-18 18:28:57 +03:30
1670c9d145 Merge upstream/main into main
Resolve conflicts while keeping field-expert lock/view fixes and user
party-access query improvements from the fork.
2026-06-18 18:27:01 +03:30
92e05d2a49 Fix async error 2026-06-18 18:22:15 +03:30
dcc3ee71de Fixed GET users 2026-06-18 18:15:20 +03:30
fa188862e5 Fixed Lock for Field expert + user view of files 2026-06-18 13:32:40 +03:30
d8f7766f10 Fixed Lock for Field expert + user view of files 2026-06-18 13:31:56 +03:30
084d0e1360 Merge pull request 'main' (#129) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#129
2026-06-17 17:00:08 +03:30
0111f3acd2 merge upstream 2026-06-17 16:59:36 +03:30
1ef17ce337 fanavaran parsian done 2026-06-17 16:57:21 +03:30
SepehrYahyaee
6df8044c5a Added new script 2026-06-17 16:39:59 +03:30
SepehrYahyaee
bc5be99b59 Fixed registrar and field expert 2026-06-17 16:39:30 +03:30
SepehrYahyaee
a4eb98258b New inquiries for parsian 2026-06-17 14:32:25 +03:30
SepehrYahyaee
ad35d35065 Fixed repetetive emails closing mongo connection 2026-06-17 12:37:18 +03:30
SepehrYahyaee
4bd88ff0dd Mirrored previous lookups for accident-ways 2026-06-16 11:31:41 +03:30
SepehrYahyaee
b008eda11b Fixed error in inquiry 2026-06-15 16:24:49 +03:30
1680fdc5b4 Added registrar + fixed conflicts
Reviewed-on: Yara724/api#124
2026-06-15 16:12:45 +03:30
SepehrYahyaee
921f9719c7 Merge upstream/main - resolve conflicts
- auth.module.ts: keep HashService (upstream accidentally removed it)
- payload.types.ts: merge both - add upstream's clientId field
- claim-request-management.module.ts: keep RegistrarClaimMirrorController
- expert-initiated-blame.mirror.controller.ts: keep our clean version

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:11:39 +03:30
SepehrYahyaee
a7849f915a Added registrar mirrored 2026-06-15 15:58:31 +03:30
SepehrYahyaee
19dc2a76f2 Added expert field mirror flow 2026-06-15 11:27:25 +03:30
SepehrYahyaee
41f81a2f76 Added expert field mirror flow 2026-06-15 11:24:41 +03:30
048398d653 merge upstream 2026-06-13 21:45:20 +03:30
SepehrYahyaee
79905345e5 Fix car-damage links 2026-06-13 17:43:33 +03:30
SepehrYahyaee
0ed7cd7012 Fix car-damage links 2026-06-13 17:43:00 +03:30
1e7b0d7d06 Merge pull request 'Fixing link for some documents' (#120) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#120
2026-06-13 17:25:25 +03:30
SepehrYahyaee
6426233350 Fix links 2026-06-13 17:24:25 +03:30
3e5e9852ad merge upstream 2026-06-13 15:52:20 +03:30
SepehrYahyaee
3d6b9e130c Fix invite second party link 2026-06-13 15:51:38 +03:30
SepehrYahyaee
ec07d42ced Fix invite second party link 2026-06-13 15:49:07 +03:30
407f58f5ee Merge pull request 'Added USER_BASE_PATH env' (#118) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#118
2026-06-13 15:30:21 +03:30
SepehrYahyaee
c8274d8435 Added USER_BASE_PATH env 2026-06-13 15:29:33 +03:30
f0b24dcd26 Merge pull request 'fixed mock data' (#117) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#117
2026-06-13 14:07:40 +03:30
SepehrYahyaee
5414d9717e Fix 2026-06-13 14:07:07 +03:30
SepehrYahyaee
e413991e7c Fixed mock data 2026-06-13 14:05:27 +03:30
b144458943 Merge pull request 'Added API for externalAPI, added env for clients' (#116) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#116
2026-06-13 11:27:13 +03:30
SepehrYahyaee
3abbd45fac Added API for externalAPI, added env for clients 2026-06-13 11:26:33 +03:30
cb47069e90 Merge pull request 'Removed access control for clients' (#115) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#115
2026-06-11 15:08:36 +03:30
3c3b5191fb Removed access control for clients 2026-06-11 15:07:33 +03:30
8053e1a088 Merge pull request 'making smsApiKey for clients not unique' (#114) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#114
2026-06-09 10:56:19 +03:30
SepehrYahyaee
d276c32e87 making smsApiKey for clients not unique 2026-06-09 10:55:28 +03:30
951ff9b50b Merge pull request 'Fixed correct clientId gets replaced in CAR_BODY' (#113) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#113
2026-06-09 10:40:43 +03:30
SepehrYahyaee
a59e4c57a5 Fixed correct clientId gets replaced in CAR_BODY 2026-06-09 10:02:21 +03:30
33c9811f61 Merge pull request 'Fixed visibility rules to match the business rules' (#112) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#112
2026-06-08 10:22:33 +03:30
SepehrYahyaee
cab584410f Fixed visibility rules to match the business rules 2026-06-08 10:22:02 +03:30
c413bd3417 Merge pull request 'Fixed insurer' (#111) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#111
2026-06-07 16:11:10 +03:30
SepehrYahyaee
249c359898 Fixed insurer 2026-06-07 16:10:42 +03:30
393d43c4d2 Merge pull request 'Fixed unified damagedParts + Simplified Captcha' (#110) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#110
2026-06-07 10:34:53 +03:30
SepehrYahyaee
34142942e5 Simplified captcha, Fixed unified damaged parts 2026-06-07 10:34:10 +03:30
SepehrYahyaee
f023d0f3e7 Simplified captcha 2026-06-06 10:45:53 +03:30
2d7afba75d merge upstream 2026-06-04 13:16:34 +03:30
SepehrYahyaee
9ee933cb76 Fixed price-drop 2026-06-03 17:04:16 +03:30
SepehrYahyaee
16c598118d New module for expert field 2026-06-03 16:51:56 +03:30
2b1edd64c1 Merge pull request 'Fix addClient bug' (#109) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#109
2026-06-03 14:01:44 +03:30
SepehrYahyaee
d92231e517 Fix addClient bug 2026-06-03 14:00:40 +03:30
dc14698823 Merge pull request 'Fixed unified data in insurer as well' (#108) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#108
2026-06-03 12:55:43 +03:30
SepehrYahyaee
8236f0440d Fixed unified data in insurer as well 2026-06-03 12:55:15 +03:30
ffcedcd5f1 Merge pull request 'YARA-948, YARA-977, + Bugs' (#107) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#107
2026-06-03 12:31:44 +03:30
SepehrYahyaee
bd5a33e2ba Fixed clientId of claim error 2026-06-03 12:30:42 +03:30
SepehrYahyaee
0b47e8789b YARA-977 2026-06-03 12:23:30 +03:30
SepehrYahyaee
2c810afcb6 YARA-948 2026-06-03 12:05:19 +03:30
SepehrYahyaee
077bae429e Fixed damagedParts unified structure and resend problems 2026-06-03 11:34:21 +03:30
456135ad08 Merge pull request 'script updated , inquiry field added to blame case and claim case' (#106) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#106
2026-06-02 12:33:50 +03:30
eae46c3212 merge upstream 2026-06-02 12:33:13 +03:30
fe82455562 script updated , inquiry field added to blame case and claim case 2026-06-02 12:32:49 +03:30
f2d7e39487 Merge pull request 'refresh script modified' (#105) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#105
2026-06-01 18:14:57 +03:30
8730e9af62 refresh script modified 2026-06-01 18:14:21 +03:30
cf07122710 Merge pull request 'Centralized car body inquiry' (#104) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#104
2026-06-01 16:38:12 +03:30
SepehrYahyaee
a0247d7769 Centralized car body inquiry 2026-06-01 16:37:23 +03:30
fcf3e63f9b Merge pull request 'refresh blame inquirys script added (look at the comments section for env)' (#103) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#103
2026-06-01 16:07:12 +03:30
519d91102f merge upstream 2026-06-01 16:02:06 +03:30
4bc5889ccd refresh blame inquirys script 2026-06-01 16:01:41 +03:30
a47c6f1c96 Merge pull request 'Fixed inquiry of birthdate' (#102) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#102
2026-06-01 14:11:57 +03:30
SepehrYahyaee
5fab0a00b6 Fixed inquiry of birthdate 2026-06-01 14:11:23 +03:30
e650abdf40 Merge pull request 'main' (#101) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#101
2026-06-01 13:21:59 +03:30
SepehrYahyaee
6261af8a29 Fixed lock 2026-06-01 13:21:32 +03:30
SepehrYahyaee
fde6464739 YARA-951 2026-06-01 13:06:09 +03:30
a07b5c3c1e Merge pull request 'Fixed sheba' (#100) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#100
2026-06-01 12:47:22 +03:30
SepehrYahyaee
06af79fa47 Fixed sheba 2026-06-01 12:46:19 +03:30
1a8181a872 Merge pull request 'YARA-972' (#99) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#99
2026-06-01 12:07:32 +03:30
SepehrYahyaee
4a189ba4ef YARA-972 2026-06-01 11:49:25 +03:30
859244940c Merge pull request 'Centralized damaged parts alongside all their required info' (#98) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#98
2026-06-01 11:38:55 +03:30
SepehrYahyaee
cec349b7c2 Centralized damaged parts alongside all their required info 2026-06-01 11:38:30 +03:30
8d8f76eadd Merge pull request 'Reactivated inquiry of birth date' (#97) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#97
2026-06-01 09:39:40 +03:30
SepehrYahyaee
b9e7373225 Reactivated inquiry of birth date 2026-06-01 09:38:56 +03:30
6ddb06594b Merge pull request 'Fixed captcha for prod' (#96) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#96
2026-05-31 16:16:30 +03:30
SepehrYahyaee
e90c6a5c50 Fixed captcha for prod 2026-05-31 16:15:55 +03:30
97f26d400f Merge pull request 'Fixed Swagger ui in prod' (#95) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#95
2026-05-31 15:04:56 +03:30
SepehrYahyaee
02a69f3db2 Fixed Swagger ui in prod 2026-05-31 15:03:46 +03:30
c137d6c6c4 Merge pull request 'Fixed Captcha' (#94) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#94
2026-05-31 14:01:51 +03:30
SepehrYahyaee
2b7192151d Fixed Captcha 2026-05-31 14:01:04 +03:30
389133e1c9 Merge pull request 'Added badane API inquiry' (#93) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#93
2026-05-30 17:17:25 +03:30
SepehrYahyaee
768d6d12fe Removed logs 2026-05-30 17:15:35 +03:30
SepehrYahyaee
84b752c6cc External API for badane 2026-05-30 17:15:21 +03:30
0622ceeaf4 Merge pull request 'Fixed resend blame and claim sms start' (#92) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#92
2026-05-30 16:18:49 +03:30
SepehrYahyaee
3b0db0d250 Fixed resend blame and claim sms start 2026-05-30 16:18:11 +03:30
c502adbe76 Merge pull request 'reduced minimum bytes of medias' (#91) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#91
2026-05-30 15:20:06 +03:30
SepehrYahyaee
7aada14551 reduced minimum bytes of medias 2026-05-30 15:19:04 +03:30
b3bf1b85f8 Merge pull request 'main' (#90) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#90
2026-05-30 15:02:35 +03:30
SepehrYahyaee
d6f1cb9eeb Fixed company code error not found 2026-05-30 15:01:53 +03:30
SepehrYahyaee
da298a3350 Showing a valid message while the OTP is still valid, instead of 400 2026-05-30 11:37:09 +03:30
722cbbf5c9 Merge pull request 'Added .env.example' (#89) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#89
2026-05-30 10:55:22 +03:30
SepehrYahyaee
245160bfc2 Added .env.example 2026-05-30 10:54:52 +03:30
9c760d59f7 Merge pull request 'ENV edit, Joi validation' (#88) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#88
2026-05-30 10:38:12 +03:30
SepehrYahyaee
6ac0bf060e Added Joi for schema validation, tidied up envs 2026-05-30 10:37:23 +03:30
10df869efb Tidied up the project 2026-05-30 08:59:57 +03:30
5c372947dd Merge pull request 'Fixed enrichedEvaluation' (#87) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#87
2026-05-26 16:36:29 +03:30
SepehrYahyaee
9003a7abb6 Fixed enrichedEvaluation 2026-05-26 16:35:13 +03:30
a994331439 Merge pull request 'Changed bcrypt to scrypt built-in' (#86) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#86
2026-05-25 16:44:46 +03:30
SepehrYahyaee
ae12049e1b Changed bcrypt to scrypt built-in 2026-05-25 16:44:43 +03:30
02031efdb5 Merge pull request 'Tidied up the packages and unused modules' (#85) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#85
2026-05-25 14:59:51 +03:30
SepehrYahyaee
48cc4d8a8d Tidied up the packages and unused modules 2026-05-25 14:59:37 +03:30
cbbb45378d Merge pull request 'YARA-885, + fixes' (#84) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#84
2026-05-25 14:15:37 +03:30
SepehrYahyaee
680f3c1798 Fixed resendCarParts label_fa's + OTP 2026-05-25 14:13:17 +03:30
SepehrYahyaee
64fa560f73 YARA-951 case-4 2026-05-25 13:11:37 +03:30
SepehrYahyaee
ff94fa35bf YARA-885 2026-05-25 12:01:00 +03:30
037d9fa934 Merge pull request 'YARA-951' (#83) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#83
2026-05-24 13:34:44 +03:30
SepehrYahyaee
2bdd0d507e YARA-951 2026-05-24 13:34:43 +03:30
f60efa52b1 Merge pull request 'FIX Captcha' (#82) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#82
2026-05-24 10:56:37 +03:30
SepehrYahyaee
866696094f FIX Catcha 2026-05-24 10:56:28 +03:30
ed936ad7b1 Merge pull request 'YARA-913' (#81) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#81
2026-05-24 10:12:31 +03:30
SepehrYahyaee
4d4106a8ab Change minimum size of files 2026-05-24 10:11:52 +03:30
SepehrYahyaee
af875a4773 YARA-913 2026-05-24 10:10:17 +03:30
91221a6848 Merge pull request 'FIX FILE SIZES' (#80) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#80
2026-05-23 16:23:15 +03:30
SepehrYahyaee
a31196774c FIX FILE SIZES 2026-05-23 16:23:11 +03:30
d2474d65bc Merge pull request 'YARA-941' (#79) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#79
2026-05-23 15:51:33 +03:30
SepehrYahyaee
94bde88cb6 YARA-941 2026-05-23 14:05:11 +03:30
39c4855b95 Merge pull request 'Deactivated Valiation Whitelist for now' (#78) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#78
2026-05-20 15:27:46 +03:30
SepehrYahyaee
35487ad033 Deactivated Valiation Whitelist for now 2026-05-20 15:27:44 +03:30
3dec22595c Merge pull request 'Fix SMS, and added pagination+sorting+searching for GETs' (#77) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#77
2026-05-20 14:57:31 +03:30
SepehrYahyaee
44723259d6 Fix SMS, and added pagination+sorting+searching for GETs 2026-05-20 14:57:10 +03:30
c96e361990 Merge pull request 'Validation for prices and objection' (#76) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#76
2026-05-20 11:09:25 +03:30
SepehrYahyaee
e4dfe7c572 Validation for prices and objection 2026-05-20 11:08:47 +03:30
511d478064 Merge pull request 'YARA-883 + Side ID fixes' (#75) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#75
2026-05-18 17:15:13 +03:30
SepehrYahyaee
cef684e37f YARA-883 + Side ID fixes 2026-05-18 17:14:45 +03:30
c81157c431 Merge pull request 'YARA-850, YARA-885' (#74) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#74
2026-05-18 11:23:11 +03:30
SepehrYahyaee
e1954cdb37 YARA-885 2026-05-18 11:00:53 +03:30
SepehrYahyaee
7ff3e9fd10 YARA-850 2026-05-18 10:06:14 +03:30
ced8586710 Merge pull request 'YARA-908' (#73) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#73
2026-05-18 09:30:09 +03:30
SepehrYahyaee
f0ba8949cb YARA-908 2026-05-18 09:29:41 +03:30
fb224360ab Merge pull request 'Fixed step for car-capture video' (#72) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#72
2026-05-17 15:39:58 +03:30
SepehrYahyaee
600c6bd7ed Fixed step for car-capture video 2026-05-17 15:39:23 +03:30
129be58cc9 Merge pull request 'Toggle External API' (#71) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#71
2026-05-16 16:23:44 +03:30
SepehrYahyaee
094816ce8f Toggle External API 2026-05-16 16:23:01 +03:30
f2848a6179 Merge pull request 'Fix 404 for invalid email of actors, added APIs for client settings' (#70) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#70
2026-05-16 15:47:59 +03:30
SepehrYahyaee
7797a4ddab Fix 404 for invalid email of actors, added APIs for client settings 2026-05-16 15:47:31 +03:30
09eb6cc5c0 Merge pull request 'Fixed 3 upload documents for capture part' (#69) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#69
2026-05-16 11:16:22 +03:30
SepehrYahyaee
7c76149c95 Fixed 3 upload documents for capture part 2026-05-16 11:16:08 +03:30
d562e09aab Merge pull request 'Fixed timing issue with UTC' (#68) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#68
2026-05-16 10:28:48 +03:30
SepehrYahyaee
2c851725a5 Fixed timing issue with UTC 2026-05-16 10:28:32 +03:30
e4d6246103 Merge pull request 'Fixed persian labels for car angles' (#67) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#67
2026-05-13 13:22:06 +03:30
SepehrYahyaee
b9d15d1ff6 Fixed persian labels for car angles 2026-05-13 13:21:53 +03:30
241634b149 Merge pull request 'YARA-898, YARA-899' (#66) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#66
2026-05-13 09:33:29 +03:30
SepehrYahyaee
7ba3b57cee Merge branch 'main' of git.ittalie.com:s.yahyaee/yara724-api 2026-05-13 09:31:27 +03:30
SepehrYahyaee
5b6409fc2e Added linkToken and linkContext to OTP 2026-05-13 09:23:45 +03:30
fd4cd3128f Merge pull request 'changed logs' (#65) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#65
2026-05-12 13:35:41 +03:30
0d0cec4b20 - Expert-entered amounts on factor validation
- Cap raised to 53,000,000
- Cap error copy
- Repriced all-factor (and any repriced completion)
- Docs added to expert-claim.v2.controller and PATCH 2/expert-claim-validate-factors/:id
2026-05-12 13:32:13 +03:30
SepehrYahyaee
e26c533a52 Fixed bugs 2026-05-12 11:07:58 +03:30
SepehrYahyaee
f75e6a4453 YARA-898 2026-05-12 11:00:18 +03:30
SepehrYahyaee
e89cf107ff YARA-899 2026-05-12 10:26:04 +03:30
07c4e5126a Merge pull request 'Fixed bugs' (#64) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#64
2026-05-10 17:01:23 +03:30
SepehrYahyaee
010846acd9 Fixed bugs 2026-05-10 17:00:41 +03:30
SepehrYahyaee
cc926d4668 deprecated some old APIs + added examples for login 2026-05-10 14:29:32 +03:30
fdb75d52f7 Merge pull request 'YARA-884' (#63) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#63
2026-05-10 14:16:05 +03:30
SepehrYahyaee
186f6c5837 YARA-884 2026-05-10 14:15:38 +03:30
SepehrYahyaee
3fb90cf1c9 YARA-886 2026-05-10 14:04:11 +03:30
5e5ad0e1b3 Merge pull request 'Added sign links and data to expert-claim API' (#62) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#62
2026-05-10 12:42:37 +03:30
SepehrYahyaee
82bb232d28 Added sign links and data to expert-claim API 2026-05-10 12:42:24 +03:30
45392ad268 Merge pull request 'YARA-877' (#61) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#61
2026-05-10 11:44:07 +03:30
SepehrYahyaee
9c62dc4d3a YARA-877 2026-05-10 11:43:28 +03:30
d1bc64bb6d Merge pull request 'Fixed sheba inquiry' (#60) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#60
2026-05-09 17:51:58 +03:30
SepehrYahyaee
be58b7e47e Fixed sheba inquiry 2026-05-09 17:51:31 +03:30
78e86e5745 Merge pull request 'Fixed external API call to personal inquiry' (#59) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#59
2026-05-09 16:17:57 +03:30
SepehrYahyaee
fe163419c0 Fixed personal inquiry 2026-05-09 16:17:28 +03:30
SepehrYahyaee
eb648d8a87 Fixed personal inquiry 2026-05-09 16:17:09 +03:30
b856cb59f9 Merge pull request 'YARA-732' (#58) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#58
2026-05-09 14:00:40 +03:30
SepehrYahyaee
a52b7a0a72 Added catalog endpoints to claim expert panel 2026-05-09 14:00:02 +03:30
SepehrYahyaee
7998649a89 Added external APIs inquiries 2026-05-09 13:48:19 +03:30
SepehrYahyaee
74c91c73b6 Moved OTP to SMS module 2026-05-09 12:36:38 +03:30
SepehrYahyaee
9e2cec5bc3 YARA-732 2026-05-09 12:09:17 +03:30
e95cb4c255 Merge pull request 'Added extra fields for insurer' (#57) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#57
2026-05-09 10:14:27 +03:30
SepehrYahyaee
c1a54baaf0 Added extra fields for insurer 2026-05-09 09:59:01 +03:30
f8193b6622 Merge pull request 'main' (#56) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#56
2026-05-05 14:53:58 +03:30
3e889307a1 merge upstream 2026-05-05 10:13:11 +03:30
Soheil Hajizadeh
972b4b8719 status modfied 2026-05-04 21:19:11 +03:30
96c21294db Merge pull request 'Fixed claim/blame damagedParts' (#55) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#55
2026-05-03 13:56:41 +03:30
c579f8fa1d Fixed claim/blame damagedParts 2026-05-03 13:54:48 +03:30
c2d59112cf Merge pull request 'user owner guidence added + status and steps fixed' (#54) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#54
2026-05-02 01:54:00 +03:30
Soheil Hajizadeh
e1115b0632 user owner guidence added + status and steps fixed 2026-05-02 01:50:45 +03:30
908292b0c3 Merge pull request 'Fix steps' (#53) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#53
2026-05-01 18:16:50 +03:30
55ec6f1fd1 Fix steps 2026-05-01 17:52:25 +03:30
d33cff8438 Merge pull request 'Fix workflow steps' (#52) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#52
2026-05-01 16:01:53 +03:30
0bb13f4596 Fix workflow steps 2026-05-01 16:01:08 +03:30
70306d42e0 Merge pull request 'Fix damaged-parts flow bug on carAngles' (#51) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#51
2026-05-01 14:45:39 +03:30
d9dc4ecdff Fix damaged-parts flow bug on carAngles 2026-05-01 14:44:28 +03:30
f66fa5d7b4 Merge pull request 'YARA-867, YARA-868' (#50) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#50
2026-05-01 11:38:50 +03:30
6429cb0f2b Fix bugs 2026-05-01 11:37:24 +03:30
6f120b0066 YARA-868 2026-05-01 11:25:10 +03:30
8419ec06ae YARA-867 2026-05-01 11:14:05 +03:30
def4023185 Merge pull request 'Fix data returning owner on claimDetails' (#49) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#49
2026-04-30 13:31:28 +03:30
40606fecf1 Fix data returning owner on claimDetails 2026-04-30 13:30:58 +03:30
c567f93e85 Merge pull request 'Added sign endpoint for claim' (#48) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#48
2026-04-30 13:18:04 +03:30
a9846095c1 Added sign endpoint for claim 2026-04-30 13:17:23 +03:30
b6f0e3c821 Merge pull request 'YARA-855, YARA-856' (#47) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#47
2026-04-30 10:34:52 +03:30
bffb8a3b97 YARA-855 2026-04-30 10:32:53 +03:30
715a9f2467 YARA-856 2026-04-30 09:57:28 +03:30
3813c2b3e3 Merge pull request 'YARA-854, YARA-848, YARA-850' (#46) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#46
2026-04-29 20:24:00 +03:30
ebf8a9a624 YARA-850 2026-04-29 20:22:43 +03:30
993d809de2 YARA-854 2026-04-29 14:26:04 +03:30
80ef885d3b Merge pull request 'Fixed label_fa' (#45) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#45
2026-04-28 16:33:36 +03:30
SepehrYahyaee
3f608f63f1 Fixed label_fa 2026-04-28 16:32:27 +03:30
6019c9e954 Merge pull request 'YARA-853, YARA-857, YARA-858 and a couple of fixes' (#44) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#44
2026-04-28 15:40:31 +03:30
SepehrYahyaee
8ae6f2c91b Fix get details for insurer 2026-04-28 15:39:06 +03:30
SepehrYahyaee
f999313476 YARA-853 2026-04-28 15:22:04 +03:30
SepehrYahyaee
98f1d2caf5 YARA-857 2026-04-28 14:41:47 +03:30
SepehrYahyaee
bbd83da2d5 YARA-858 2026-04-28 14:27:12 +03:30
SepehrYahyaee
9296795166 Added factorLink and branchName support 2026-04-28 13:31:02 +03:30
SepehrYahyaee
f456443342 Fixed blame resend 2026-04-28 10:15:03 +03:30
SepehrYahyaee
bcedd8c6f3 Added GET car-other-parts in v2 as well; deprecated old endpoints and reordered swagger documents 2026-04-28 10:01:45 +03:30
8caf13cf18 Merge pull request 'YARA-833' (#43) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#43
2026-04-27 17:04:18 +03:30
SepehrYahyaee
e90f8dc33c YARA-833 2026-04-27 17:03:29 +03:30
4c4b1a1db7 Merge pull request 'Fixed legacy requestedCounts methods and statistics + claimLink address' (#42) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#42
2026-04-27 15:30:21 +03:30
SepehrYahyaee
c2f996cc28 Fixed legacy requestedCounts methods and statistics + claimLink address 2026-04-27 15:29:44 +03:30
362e02ddc4 Merge pull request 'Fix locks' (#41) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#41
2026-04-26 16:09:17 +03:30
SepehrYahyaee
4b0e8a3547 FIX LOCKS 2026-04-26 16:08:14 +03:30
SepehrYahyaee
885678df7d Fixed lock mechanism, and some resendItem types 2026-04-26 15:58:21 +03:30
8dffc46357 Merge pull request 'YARA-725, YARA-830, YARA-832' (#40) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#40
2026-04-26 11:44:54 +03:30
SepehrYahyaee
b5b3b722c6 YARA-725 2026-04-26 11:43:17 +03:30
SepehrYahyaee
4f8cb43883 YARA-832 2026-04-26 09:31:50 +03:30
SepehrYahyaee
6c2d178686 YARA-830 2026-04-26 09:16:58 +03:30
9854a58282 Merge pull request 'Fix' (#39) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#39
2026-04-25 17:38:23 +03:30
SepehrYahyaee
7184142137 Fix 2026-04-25 17:38:01 +03:30
d45975b6a7 Merge pull request 'YARA-837, YARA-836' (#38) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#38
2026-04-25 17:12:59 +03:30
SepehrYahyaee
05c5b70b4d YARA-837 2026-04-25 17:10:07 +03:30
SepehrYahyaee
5d2227b00b YARA-836 + 2 more sms 2026-04-25 16:53:56 +03:30
eef305c42e Merge pull request 'Added required documents for blame, added 2 sms' (#37) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#37
2026-04-25 15:15:11 +03:30
SepehrYahyaee
9c87927e6c Added required documents for blame, added 2 sms 2026-04-25 15:14:44 +03:30
5618d120e3 Merge pull request 'YARA-834' (#36) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#36
2026-04-22 15:03:28 +03:30
SepehrYahyaee
d9b1537ee4 Centralized SMS services YARA-834 2026-04-22 15:02:08 +03:30
f01882dc3a Merge pull request 'Added SMS for Link flow of FIELD-EXPERT: YARA-802' (#35) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#35
2026-04-22 11:14:08 +03:30
SepehrYahyaee
8284aba825 Added SMS for Link flow of FIELD-EXPERT: YARA-802 2026-04-22 11:13:47 +03:30
3c15901ecc Merge pull request 'Fixing createdAtFa' (#34) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#34
2026-04-22 10:35:39 +03:30
SepehrYahyaee
1e1edf9136 Fixing createdAtFa 2026-04-22 10:35:00 +03:30
daad9539ea Merge pull request 'YARA-821' (#33) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#33
2026-04-22 10:30:05 +03:30
SepehrYahyaee
194b71cdb2 YARA-821 2026-04-22 10:29:01 +03:30
SepehrYahyaee
04ba46ed2a Merge branch 'main' of git.ittalie.com:s.yahyaee/yara724-api 2026-04-22 09:11:42 +03:30
c6f7edabd2 Merge pull request 'daghi problem fixed , 2 apis for reporting counts added' (#32) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#32
2026-04-21 16:44:28 +03:30
75b7e5ecad daghi problem fixed , 2 apis for reporting counts added 2026-04-21 16:43:15 +03:30
a3156881b8 Merge pull request 'daghi + expert and damage expert name in decision +' (#31) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#31
2026-04-20 18:20:34 +03:30
41c44dcf23 daghi + expert and damage expert name in decision +
signature required added
2026-04-20 18:19:25 +03:30
SepehrYahyaee
95bfc095ce Fixed some bugs on FaTimestamps 2026-04-20 12:11:12 +03:30
b63c2155bc Merge pull request 'blame status agreement fixed' (#30) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#30
2026-04-20 11:22:04 +03:30
9463142ecf blame status agreement fixed 2026-04-20 11:19:49 +03:30
a71c4ea980 Merge pull request 'YARA-814' (#29) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#29
2026-04-20 10:49:28 +03:30
SepehrYahyaee
b826a133a4 YARA-814 2026-04-20 10:49:06 +03:30
b4501c503e Merge pull request 'main' (#28) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#28
2026-04-20 10:02:28 +03:30
SepehrYahyaee
a0d337aff0 Merge remote-tracking branch 'origin/main' 2026-04-20 10:00:02 +03:30
28e2de459a Merge pull request 'main' (#27) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#27
2026-04-20 09:53:53 +03:30
af1a07743e merge upstream 2026-04-19 17:23:47 +03:30
fca88bc151 update the resend request and user side 2026-04-19 17:23:14 +03:30
SepehrYahyaee
e264695db3 YARA-803 2026-04-19 13:08:47 +03:30
277f2d4d66 Merge pull request 'Fixed lock mechanism for damage-expert' (#26) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#26
2026-04-19 12:32:22 +03:30
SepehrYahyaee
514018beb1 Fixed lock mechanism for damage-expert 2026-04-19 12:32:03 +03:30
374b15933d Merge pull request 'add blame in claim details plus auto expert decision bug fixed' (#25) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#25
2026-04-19 11:59:33 +03:30
08a4d754c1 add blame in claim details plus auto expert decision bug fixed 2026-04-19 11:57:18 +03:30
c5b2d7b520 Merge pull request 'Fixed steps' (#24) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#24
2026-04-19 10:42:05 +03:30
SepehrYahyaee
5a84080bf1 Fixed steps 2026-04-19 10:41:51 +03:30
68b3b2000b Merge pull request 'Fixed statuses and switched some' (#23) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#23
2026-04-18 17:42:27 +03:30
SepehrYahyaee
028ca899fe resolved conflicts and maintained new features 2026-04-18 17:41:45 +03:30
SepehrYahyaee
0086b8db4d fixed many bugs and step issues 2026-04-18 17:04:19 +03:30
SepehrYahyaee
7f5b64f2a6 Fixed DTO 2026-04-18 14:02:50 +03:30
2898897dd9 Merge pull request 'lookups api is offline' (#22) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#22
2026-04-18 13:29:11 +03:30
e8a3873851 lookups api is offline 2026-04-18 13:26:37 +03:30
131df63a3f Merge pull request 'main' (#21) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#21
2026-04-18 12:35:41 +03:30
b9eb0ab5bd Merge remote-tracking branch 'upstream/main' | Merge upstream into main and reapply local changes 2026-04-18 12:30:32 +03:30
4bdb9fd469 update some important issues 2026-04-18 10:49:22 +03:30
38f700c3d7 Merge pull request 'Fixed bugs of car-body steps' (#19) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#19
2026-04-18 10:10:58 +03:30
SepehrYahyaee
1a1d55bc2e Fixed bugs of car-body steps 2026-04-18 10:10:19 +03:30
a2f1d5ea7f Merge pull request 'YARA-725' (#18) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#18
2026-04-15 12:32:53 +03:30
SepehrYahyaee
9a65071276 YARA-725 2026-04-15 12:32:25 +03:30
12d6fa4d73 Merge pull request 'YARA-784, YARA-789, YARA-791' (#17) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#17
2026-04-15 11:58:36 +03:30
SepehrYahyaee
640b240ada YARA-791 2026-04-15 11:57:09 +03:30
SepehrYahyaee
f8fbbb7ac6 YARA-784 2026-04-15 11:43:19 +03:30
SepehrYahyaee
0e2789c209 YARA-789 2026-04-15 10:16:18 +03:30
7661862564 Merge pull request 'Fixed reports+insurer + fixed field_expert not accessing claim APIs temporarily' (#16) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#16
2026-04-13 14:10:39 +03:30
SepehrYahyaee
a2143fe1c5 Fixed field_expert not accessing claim apis 2026-04-13 14:09:29 +03:30
Soheil Hajizadeh
494e3d93ab update the expert claim dto 2026-04-11 09:46:54 +03:30
9afb079786 Rework the reports and insurer part 2026-04-11 00:12:10 +03:30
25958bb966 merge upstream 2026-04-08 16:09:14 +03:30
Soheil Hajizadeh
1d51370b3e vehicle data added to get single request by expert , also submit in person 2026-04-08 16:08:31 +03:30
e44721b7be Merge pull request 'YARA-743, YARA-763, YARA-764' (#15) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#15
2026-04-06 15:00:02 +03:30
7ef057dc1f YARA-764 2026-04-06 14:33:33 +03:30
1b68a81204 YARA-763 2026-04-06 14:15:31 +03:30
f77da50d1f YARA-743 2026-04-06 14:01:45 +03:30
79c2982e19 Merge pull request 'Fixed log for error' (#14) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#14
2026-04-05 16:45:12 +03:30
fb4d166c31 Fixed log for error 2026-04-05 16:44:14 +03:30
9084f8fafb Merge pull request 'update the user side from the list of the user requests' (#13) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#13
2026-04-04 14:03:18 +03:30
b216a363fb merge upstream 2026-03-29 12:22:53 +03:30
Soheil Hajizadeh
b2391751f0 update the user side for the requests list 2026-03-29 12:21:01 +03:30
df6d2bd04d Merge pull request 'Mocked all external API calls' (#12) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#12
2026-03-29 11:48:37 +03:30
f277d87dfa Mocked all external API calls 2026-03-29 11:46:56 +03:30
672ec25438 Merge pull request 'Fixed signature error for car-body' (#11) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#11
2026-03-29 10:43:33 +03:30
aa871c86f6 Fixed signature error for car-body 2026-03-29 10:41:38 +03:30
553a34054c Merge pull request 'Added registrar + fixed sign bug in car-body flow' (#10) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#10
2026-03-26 16:37:33 +03:30
8af152abc0 Added registrar + fixed sign bug in car-body flow 2026-03-26 16:35:02 +03:30
0446c83b36 Merge pull request 'update the blame service' (#9) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#9
2026-03-18 15:53:40 +03:30
255cfd3eb3 Merge pull request 'Disabled Inquiry' (#8) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#8
2026-03-17 18:55:18 +03:30
b1be5b1e09 Disabled Inquiry 2026-03-17 18:53:47 +03:30
7d3565ec51 Merge pull request 'Added Expert initiated flow (field expert) + deactivated inquiry' (#7) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#7
2026-03-16 15:45:49 +03:30
fc599acf4a Added Field Expert flows, and deactivated inquiry 2026-03-16 15:39:39 +03:30
SepehrYahyaee
b40270f058 Added refactored version of car-body 2026-03-14 13:36:05 +03:30
Soheil Hajizadeh
8d6fa3daac update the blame service 2026-03-10 01:06:30 +03:30
64dfd1ca8a Merge pull request 'historical refactored yara724 merge pull request' (#6) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#6
2026-02-24 12:24:56 +03:30
665ed9e70f Merge pull request 'blame and claim refactored' (#1) from integrate-my-work into main
Reviewed-on: #1
2026-02-24 12:21:43 +03:30
422 changed files with 83550 additions and 18202 deletions

100
.env.example Normal file
View File

@@ -0,0 +1,100 @@
# ---------------------------------------------
# 🔧 Application Environment
# ---------------------------------------------
NODE_ENV =
PORT =
CLIENT_ID =
CLIENT_NAME =
FANAVARAN_CLIENT=parsian
INSURANCE_CORP_ID='شرکت بيمه پارسيان(بيمه گر)'
CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED=false
CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN=53000000
# ---------------------------------------------
# 🌐 Application URLs
# ---------------------------------------------
URL =
USER_BASE_PATH =
BASE_URL_DEV =
# ---------------------------------------------
# 📄 Swagger / API Documentation
# ---------------------------------------------
SWAGGER_USER_DEV =
SWAGGER_PASSWORD_DEV =
# ---------------------------------------------
# 🗄️ Database (MongoDB)
# ---------------------------------------------
MONGO_HOST =
MONGO_PORT =
MONGO_USER =
MONGO_PASS =
MONGO_DB_NAME =
MONGO_OPTIONS =
MONGO_TLS =
MONGO_TLS_ALLOW_INVALID_CERTS =
MONGO_URI = 'mongodb://${MONGO_USER}:${MONGO_PASS}@${MONGO_HOST}:${MONGO_PORT}/${MONGO_DB_NAME}?${MONGO_OPTIONS}'
# ---------------------------------------------
# 🔐 Authentication / Security
# ---------------------------------------------
JWT_SECRET =
JWT_EXPIRY =
CAPTCHA_ENABLED = true
# ---------------------------------------------
# 🧩 SanHub Microservice
# ---------------------------------------------
SANHUB_BASE_URL =
SANHUB_URL_LOGIN =
SANHUB_USERNAME =
SANHUB_PASSWORD =
# ---------------------------------------------
# 🤖 AI Services
# ---------------------------------------------
AI_URL =
AI_URL_V2 =
AI_USERNAME =
AI_PASSWORD =
# ---------------------------------------------
# 📩 SMS
# ---------------------------------------------
SMS_PROVIDER =
SMS_API_KEY =
AUTH_SMS_TEMPLATE =
EXP_OTP_TIME =
FAKE_OTP =
# ---------------------------------------------
# 🌐 Proxy Configuration (Local Development Only)
# ---------------------------------------------
# NOTE: These proxy settings are for local development only.
# When deploying to the server, comment out or remove these lines
# as the server IP is already whitelisted by Fanavaran.
# SOCKS_PROXY_HOST = localhost
# SOCKS_PROXY_PORT = 6565
# ---------------------------------------------
# 🏢 Fanavaran Insurance Corp
# ---------------------------------------------
# Caption from Fanavaran insurance-corp lookup used to resolve InsuranceCorpId
# for damage-case payloads. Must match a Caption in the insurance-corp code-list.
# Example: "شرکت بيمه تجارت نو"
INSURANCE_CORP_ID =
# ---------------------------------------------
# ⚙️ Application Features / Flags
# ---------------------------------------------
AUTO_CLIENT_KEY_ENABLED =
# ---------------------------------------------
# 🔗 Other Internal Services
# ---------------------------------------------
TEJARAT_INQUIRY_EMAIL =
TEJARAT_INQUIRY_PASSWORD =
PARSIAN_API_KEY =
PARSIAN_BASIC_TOKEN =
PARSIAN_SMS_URL =

43
.gitignore vendored
View File

@@ -16,6 +16,7 @@ pids
*.pid
*.seed
*.pid.lock
files/fanavaran-auth/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
@@ -80,6 +81,9 @@ web_modules/
.env.production.local
.env.local
.env.development.env
scripts/data/fanavaran-flow.*.env
!scripts/data/fanavaran-flow.env.example
files/fanavaran-flow/
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
@@ -143,4 +147,43 @@ dist
/docker
.development.env
*.env
!scripts/data/fanavaran-flow.env.example
/files
*.jpg
*.jpeg
*.png
*.gif
*.bmp
*.tiff
*.ico
*.webp
*.svg
*.heic
*.heif
*.heif-srgb
*.heif-srgb-alpha
*.heif-srgb-alpha-heic
*.mp3
*.mp4
*.wav
*.ogg
*.flac
*.aac
*.m4a
*.m4v
*.m4b
*.m4p
*.m4r
*.m4w
*.m4x
*.txt
*.md
!README.md
!docs/
!docs/**/*.md
*.sh
!scripts/fanavaran-flow-test.sh
!scripts/fanavaran-auth.sh
*.json

142
.woodpecker.yml Normal file
View File

@@ -0,0 +1,142 @@
# yara724/api — development deployment (Deploy-Develop)
# Manual tasks: see ci-cd/TASK.md
# Requires: repo marked Trusted in Woodpecker (host volume mounts)
when:
- event: push
branch: main
- event: manual
skip_clone: true
variables:
- &host_workspace /data/1-deploy/gitea/yara724/api
- &workspace_volume /data/1-deploy/gitea/yara724/api:/workspace
- &host_ssh /home/talieh/.ssh:/root/.ssh:ro
- &pipeline_env
WORKSPACE: *host_workspace
PROJECT_NAME: Yara724 API
ENVIRONMENT: Development
APPLICATION_URL: https://y724-user.ittalie.ir/api
GIT_COMMIT_URL: https://git.ittalie.com/Yara724/api/commit/
GIT_BRANCH: main
COMPOSE_FILE: docker-compose.yml
SUCCESS_COLOR: "#36a64f"
FAILURE_COLOR: "#dc3545"
steps:
pull:
image: docker.arvancloud.ir/alpine/git:latest
environment:
<<: *pipeline_env
GIT_SSH_COMMAND: ssh -o UserKnownHostsFile=/tmp/known_hosts -o StrictHostKeyChecking=yes
volumes:
- *workspace_volume
- *host_ssh
commands:
- git config --global --add safe.directory /workspace
- mkdir -p /tmp && ssh-keyscan -H git.ittalie.com >> /tmp/known_hosts
- cd /workspace
- git pull origin main
- date +%s > /workspace/.wp-deploy-start
deploy:
image: docker.arvancloud.ir/docker:24-cli
environment:
<<: *pipeline_env
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- *workspace_volume
commands:
- cd /workspace
- docker compose -f docker-compose.yml up -d --build
notify-success:
image: docker.arvancloud.ir/alpine:3.20
environment:
<<: *pipeline_env
ROCKETCHAT_WEBHOOK:
from_secret: rocketchat_webhook
volumes:
- *workspace_volume
when:
- status: success
commands:
- apk add --no-cache curl jq git > /dev/null
- |
set -euo pipefail
git config --global --add safe.directory /workspace
cd /workspace
COMMIT_HASH="$(git rev-parse --short HEAD)"
DEPLOY_START="$(cat /workspace/.wp-deploy-start)"
DEPLOY_END="$(date +%s)"
DEPLOY_DURATION="$((DEPLOY_END - DEPLOY_START))"
COMMIT_1="$(git log -1 --pretty=format:'%s')"
COMMIT_2="$(git log -2 --pretty=format:'%s' | tail -n1)"
COMMIT_3="$(git log -3 --pretty=format:'%s' | tail -n1)"
TITLE="✅ $PROJECT_NAME - $ENVIRONMENT ✅"
TEXT="🌐 **URL**: $APPLICATION_URL
🔖 **Commit Hash**: [$COMMIT_HASH]($GIT_COMMIT_URL$COMMIT_HASH)
📝 **Recent Changes**:
• $COMMIT_1
• $COMMIT_2
• $COMMIT_3
⏱️ **Deployment Duration**: $${DEPLOY_DURATION}s"
payload="$(jq -n \
--arg title "$TITLE" \
--arg text "$TEXT" \
--arg color "$SUCCESS_COLOR" \
'{text: $title, attachments: [{text: $text, color: $color}]}')"
curl -fsS -H "Content-Type: application/json" -d "$payload" "$ROCKETCHAT_WEBHOOK" >/dev/null
rm -f /workspace/.wp-deploy-start
notify-failure:
image: docker.arvancloud.ir/alpine:3.20
environment:
<<: *pipeline_env
ROCKETCHAT_WEBHOOK:
from_secret: rocketchat_webhook
volumes:
- *workspace_volume
when:
- status: failure
commands:
- apk add --no-cache curl jq git > /dev/null
- |
set -euo pipefail
git config --global --add safe.directory /workspace
cd /workspace
COMMIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
TITLE="💥 $PROJECT_NAME - $ENVIRONMENT 💥"
TEXT="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌐 **Application URL**
$APPLICATION_URL
🔖 **Commit**
\`$COMMIT_HASH\`
⚠️ **Pipeline failed** — check Woodpecker for the failing step.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
payload="$(jq -n \
--arg title "$TITLE" \
--arg text "$TEXT" \
--arg color "$FAILURE_COLOR" \
'{text: $title, attachments: [{text: $text, color: $color}]}')"
curl -fsS -H "Content-Type: application/json" -d "$payload" "$ROCKETCHAT_WEBHOOK" >/dev/null || true
rm -f /workspace/.wp-deploy-start

10
LICENSE
View File

@@ -1,10 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -1,2 +1,5 @@
# api
# YARA724 API
## Documentation
- [Fanavaran integration](docs/fanavaran/README.md)

BIN
assets/Vazirmatn-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
---
last_updated: 2026-08-08
tags: [fanavaran, documentation]
source: fanavaran-module-docs
---
# Fanavaran documentation brief (moved)
The implementation reference for Fanavaran (Parsian template, multi-tenant) now lives under:
**[docs/fanavaran/README.md](./fanavaran/README.md)**
That set covers the original goals:
1. Write APIs (auth + GEN.03/07/08/12)
2. Read/lookup APIs
3. Constants
4. Tenant vs shared matrix
5. Full claim flow + diagrams
6. YARA integration points
7. Data mapping
8. System dependencies
9. Error handling
10. Testing
11. New-client onboarding checklist
Related: [external-api-curls.md](./external-api-curls.md), `.agents/skills/fanavaran-apis/references/third-party-cases.md`.

742
docs/external-api-curls.md Normal file
View File

@@ -0,0 +1,742 @@
# External API Curl Guide
This file documents outbound HTTP calls made by the app or maintenance scripts.
Client credentials that are hardcoded in the codebase are filled in below. Keep
placeholders only for runtime data such as national codes, plate values, tokens
returned by login calls, and payload files.
## Common Notes
- Internal app URLs in Swagger, localhost examples, and file download URLs are not listed.
- Package/build-time network calls such as npm registry, Sonar, and Docker setup are not runtime app requests.
- `firstValueFrom(this.httpService...)`, `lastValueFrom(this.httpService...)`, and `fetch(...)` call sites were checked.
- Several integrations cache bearer tokens in memory for about 55 minutes.
- Some Fanavaran credentials and the Map.ir API key are hardcoded in source. Treat them as sensitive and consider moving/rotating them.
## Fanavaran API Manager
Host:
```sh
FANAVARAN_BASE_URL="https://apimanager.iraneit.com/BimeApiManager/api"
FANAVARAN_BIME_URL="$FANAVARAN_BASE_URL/BimeApi/v2.0"
```
Used by:
- `src/fanavaran/fanavaran-lookup.service.ts`
- `src/fanavaran/fanavaran-lookup.config.ts`
- `src/claim-request-management/claim-request-management.service.ts`
### Sequence
1. Get `appToken`.
2. Login with `appToken` to get `authenticationToken`.
3. Call lookup, policy inquiry, or submit endpoint with `authenticationToken`, `CorpId`, `ContractId`, and `Location`.
### Auth Script
Use this when you only need fresh Fanavaran tokens and do not want to copy the
curl commands manually:
```sh
scripts/fanavaran-auth.sh tejaratno
scripts/fanavaran-auth.sh parsian
```
The script stores raw responses and reusable variables under
`files/fanavaran-auth/<client>/`. For example:
```sh
source files/fanavaran-auth/tejaratno/tokens.env
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/accident-causes" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
```
### Tenant Credentials
The app supports these Fanavaran client profiles:
| Client | appname | secret | userName | password | CorpId | ContractId | Location |
| --- | --- | --- | --- | --- | --- | --- | --- |
| tejaratno | `fanhab` | `5Fa@N#A2B` | `fanhabUser` | `Fan#@2U$3er` | `3539` | `263` | `100` |
| parsian | `ParsianService` | `P@r30@n$erv!ce` | `ParsianServiceUser` | `P@r30@n123` | `543` | `28` | `210050` |
Set the client variables before running. These values come from
`src/core/config/fanavaran-client.config.ts` and match
`src/claim-request-management/claim-request-management.service.ts`.
Tejaratno:
```sh
FANAVARAN_CLIENT="tejaratno"
APP_NAME='fanhab'
APP_SECRET='5Fa@N#A2B'
FANAVARAN_USERNAME='fanhabUser'
FANAVARAN_PASSWORD='Fan#@2U$3er'
CORP_ID='3539'
CONTRACT_ID='263'
LOCATION='100'
```
Parsian:
```sh
FANAVARAN_CLIENT="parsian"
APP_NAME='ParsianService'
APP_SECRET='P@r30@n$erv!ce'
FANAVARAN_USERNAME='ParsianServiceUser'
FANAVARAN_PASSWORD='P@r30@n123'
CORP_ID='543'
CONTRACT_ID='28'
LOCATION='210050'
```
### 1. Get App Token
The app sends an empty string body, deletes `Content-Type`, and keeps
`Content-Length: 0`.
```sh
curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
```
The token is returned in a response header named `appToken` or `apptoken`.
```sh
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
printf 'APP_TOKEN=%s\n' "$APP_TOKEN"
```
### 2. Login
Use the `APP_TOKEN` returned by the immediately previous GetAppToken request.
Do not reuse an old app token pasted from logs or another machine; the app does
not do that.
```sh
curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
```
The token is returned in a response header or body field named `authenticationToken`, `authenticationtoken`, or `authentication_token`.
```sh
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'AUTHENTICATION_TOKEN=%s\n' "$AUTHENTICATION_TOKEN"
```
If login returns `نام کاربر یا رمز عبور صحیح نیست` for `tejaratno`, first check
that `APP_TOKEN` was generated with `appname: fanhab` and `secret: 5Fa@N#A2B` in
the same sequence. The runtime code calls `GetAppToken` first, then passes that
fresh header value to `Login`; it does not use a static value such as
`182197f6-7b41-47b4-9b18-5bfcbc027f2e`.
### Ready-To-Run Auth Sequences
Tejaratno:
```sh
FANAVARAN_BASE_URL="https://apimanager.iraneit.com/BimeApiManager/api"
FANAVARAN_BIME_URL="$FANAVARAN_BASE_URL/BimeApi/v2.0"
FANAVARAN_CLIENT="tejaratno"
APP_NAME='fanhab'
APP_SECRET='5Fa@N#A2B'
FANAVARAN_USERNAME='fanhabUser'
FANAVARAN_PASSWORD='Fan#@2U$3er'
CORP_ID='3539'
CONTRACT_ID='263'
LOCATION='100'
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'APP_TOKEN=%s\nAUTHENTICATION_TOKEN=%s\n' "$APP_TOKEN" "$AUTHENTICATION_TOKEN"
```
Parsian:
```sh
FANAVARAN_BASE_URL="https://apimanager.iraneit.com/BimeApiManager/api"
FANAVARAN_BIME_URL="$FANAVARAN_BASE_URL/BimeApi/v2.0"
FANAVARAN_CLIENT="parsian"
APP_NAME='ParsianService'
APP_SECRET='P@r30@n$erv!ce'
FANAVARAN_USERNAME='ParsianServiceUser'
FANAVARAN_PASSWORD='P@r30@n123'
CORP_ID='543'
CONTRACT_ID='28'
LOCATION='210050'
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'APP_TOKEN=%s\nAUTHENTICATION_TOKEN=%s\n' "$APP_TOKEN" "$AUTHENTICATION_TOKEN"
```
### 3A. Lookup Endpoints
These are fetched on demand and cached under `files/fanavaran-lookups/<client>/`.
```sh
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/accident-causes" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/code-list/accident-report-type" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/vehicle-use-types" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/code-list/dmg-pay-method" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/driving-licence-types" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/code-list/accident-culprit-type" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/vehicle-kinds" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
curl -X GET "$FANAVARAN_BIME_URL/car/vehicles/inquiry-by-vin?vin=IRNKAEK4150012345" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
```
### 3B. Policy Inquiry By National Code
Used before Fanavaran claim submit to resolve a `PolicyId` when possible.
```sh
NATIONAL_CODE="<insurer national code>"
curl -X GET "$FANAVARAN_BIME_URL/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=$NATIONAL_CODE" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
```
### 3C. Third-Party Car Financial Claim Submit
`fanavaranData` is built internally from a claim case/request. The shape is large; capture an app log or preview output and save it as JSON before replaying.
```sh
curl -X POST "$FANAVARAN_BIME_URL/car/third-party-car-financial-claims" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json" \
--data @fanavaran-claim-submit.json
```
## Tejarat Inquiry Provider
Default base URL:
```sh
TEJARAT_INQUIRY_BASE_URL="${TEJARAT_INQUIRY_BASE_URL:-http://82.99.202.245:3027}"
TEJARAT_INQUIRY_EMAIL='xxx@example.com'
TEJARAT_INQUIRY_PASSWORD='123321'
```
Used by `src/sand-hub/sand-hub.service.ts` for third-party plate and car-body plate inquiries when ESG is not selected.
### Sequence
1. Login to `/user/login`.
2. Use returned `accessToken` as `Authorization: Bearer ...`.
3. Call inquiry endpoint.
### Login
```sh
curl -X POST "$TEJARAT_INQUIRY_BASE_URL/user/login" \
-H "Accept: */*" \
-H "Content-Type: application/json" \
--data '{
"email": "'"$TEJARAT_INQUIRY_EMAIL"'",
"password": "'"$TEJARAT_INQUIRY_PASSWORD"'"
}'
```
```sh
TEJARAT_ACCESS_TOKEN="<response accessToken>"
```
### Third-Party Plate / Policy Block Inquiry
```sh
curl -X POST "$TEJARAT_INQUIRY_BASE_URL/block-inquiry-tejarat" \
-H "Authorization: Bearer $TEJARAT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"leftTwoDigits": "12",
"serialLetter": "ب",
"threeDigits": "345",
"rightTwoDigits": "67",
"nationalCode": "0012345678"
}'
```
### Car-Body Plate Inquiry
```sh
curl -X POST "$TEJARAT_INQUIRY_BASE_URL/block-inquiry-tejarat/badane" \
-H "Authorization: Bearer $TEJARAT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"part1": 12,
"part2": "ب",
"part3": 345,
"part4": 67,
"nationalCode": "0012345678"
}'
```
## ESG Inquiry Provider
Default/fallback base URL in some call sites:
```sh
ESG_URL="${ESG_URL:-http://192.168.20.22:8085}"
```
Used by `src/sand-hub/sand-hub.service.ts` for selected tenants, for example when `CLIENT_ID=8`.
### Sequence
1. Login to `/auth/login`.
2. Use returned `accessToken` as `Authorization: Bearer ...`.
3. Call the inquiry endpoint.
### Login
```sh
curl -X POST "$ESG_URL/auth/login" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data '{
"username": "'"$ESG_USERNAME"'",
"password": "'"$ESG_PASSWORD"'"
}'
```
```sh
ESG_ACCESS_TOKEN="<response accessToken>"
```
### Policy By Plate
```sh
curl -X POST "$ESG_URL/inquiry/policyByPlate" \
-H "Authorization: Bearer $ESG_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"nationalCode": "0012345678",
"plk1": "12",
"plk2": "ب",
"plk3": "345",
"plksrl": "67"
}'
```
### Person Inquiry
ESG expects Jalali birth date, normalized as `YYYY-MM-DD`.
```sh
curl -X POST "$ESG_URL/inquiry/person" \
-H "Authorization: Bearer $ESG_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"nationalCode": "0012345678",
"birthDate": "1378-11-24",
"dateHasPostfix": 0
}'
```
### Sheba Validation
```sh
curl -X POST "$ESG_URL/inquiry/sheba" \
-H "Authorization: Bearer $ESG_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"accountOwnerType": "1",
"nationalCode": "0012345678",
"legalId": "",
"sheba": "IR000000000000000000000000"
}'
```
## SandHub Provider
Used by `src/sand-hub/sand-hub.service.ts` for legacy inquiry flows.
Environment:
```sh
SANHUB_BASE_URL='http://82.99.202.245:3027'
SANHUB_URL_LOGIN='http://82.99.202.245:3027/user/login'
SANHUB_USERNAME='default@admin.com'
SANHUB_PASSWORD='123321'
```
### Sequence
1. Login through `SANHUB_URL_LOGIN`.
2. Use returned `accessToken` as `Authorization: Bearer ...`.
3. Call the required endpoint under `SANHUB_BASE_URL`.
### Login
```sh
curl -X POST "$SANHUB_URL_LOGIN" \
-H "Content-Type: application/json" \
--data '{
"email": "'"$SANHUB_USERNAME"'",
"password": "'"$SANHUB_PASSWORD"'"
}'
```
```sh
SANHUB_ACCESS_TOKEN="<response accessToken>"
```
### Third-Party Plate / Policy Block Inquiry
```sh
curl -X POST "$SANHUB_BASE_URL/block-inquiry-tejarat" \
-H "Authorization: Bearer $SANHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"leftTwoDigits": "12",
"serialLetter": "ب",
"threeDigits": "345",
"rightTwoDigits": "67",
"nationalCode": "0012345678"
}'
```
### Personal Inquiry
The app converts Jalali birth dates to Gregorian before sending to SandHub.
```sh
curl -X POST "$SANHUB_BASE_URL/personal-inquiry/tejarat-no" \
-H "Authorization: Bearer $SANHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"nationalCode": "0012345678",
"birthdate": "1999-02-13"
}'
```
### Driving License Check
```sh
curl -X POST "$SANHUB_BASE_URL/driver-license-check" \
-H "Authorization: Bearer $SANHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"driverLicenseNumber": "1234567890",
"nationalCode": "0012345678"
}'
```
### Car Ownership
```sh
curl -X POST "$SANHUB_BASE_URL/ownership" \
-H "Authorization: Bearer $SANHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"Plk1": "12",
"Plk2": "ب",
"Plk3": "345",
"plkSrl": "67",
"nationalCode": "0012345678"
}'
```
### Sheba Validation
```sh
curl -X POST "$SANHUB_BASE_URL/sheba/sheba-tejaratno" \
-H "Authorization: Bearer $SANHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"AccountOwnerType": "1",
"NationalId": "0012345678",
"ShebaId": "IR000000000000000000000000"
}'
```
## Map.ir Reverse Geocoding
Used by `src/claim-request-management/claim-request-management.service.ts`.
```sh
MAP_IR_API_KEY='eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2In0.eyJhdWQiOiIyMTcxOCIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2IiwiaWF0IjoxNjgwNjA4NTkxLCJuYmYiOjE2ODA2MDg1OTEsImV4cCI6MTY4MzIwMDU5MSwic3ViIjoiIiwic2NvcGVzIjpbImJhc2ljIl19.rTviLd8b5yTHUDa3ODZyva593eMnL0d3XPg3sKkZxMOf_jNIH6lFQyIfbId-wsd1EAdsOdsL3CME_Y8t332PWJbxMNgnEq4Rf2IkClkvkSx6Sb5_4bmlhBM75zw2SmccvgbFUn4xkTOw0FT4vABC2Y3-MKctjMpmO8QOrVULSKt4psrmQhr7hBu7YRDnAAEc6muZ1VpRvdB1kqNKddoSIrfDaq6aDRJ-BNbGRAaFFvP_kH4cgSCKV4dU0TknL3mRKUiVy6_TDkjtzAN8fE2wsdvNo2pGTJPzKFsR2ipgGNTvB__g3bOnVpKsgFXPBH0e_Qa7ff1tZ3VGWy3jRNh9Lg'
LAT="35.6892"
LON="51.3890"
curl -X GET "https://map.ir/fast-reverse?lat=$LAT&lon=$LON" \
-H "accept: application/json" \
-H "x-api-key: $MAP_IR_API_KEY"
```
## SMS Providers
### Kavenegar
Used by `src/sms-orchestration/provider/kavenegar.service.ts`.
```sh
SMS_API_KEY='75776C717969412B4B52306A5956462F4A714E6F6C65544D6A2B654B7566786E'
KAVENEGAR_BASE_URL="https://api.kavenegar.com/v1/$SMS_API_KEY"
```
Send SMS:
```sh
curl -X POST -G "$KAVENEGAR_BASE_URL/sms/send.json" \
--data-urlencode "receptor=09120000000" \
--data-urlencode "message=Hello" \
--data-urlencode "sender=<optional sender>"
```
Verify lookup:
```sh
curl -G "$KAVENEGAR_BASE_URL/verify/lookup.json" \
--data-urlencode "receptor=09120000000" \
--data-urlencode "token=123456" \
--data-urlencode "template=<template>" \
--data-urlencode "token2=<optional token2>" \
--data-urlencode "token3=<optional token3>"
```
### Parsian SMS Gateway
Used by `src/sms-orchestration/provider/parsian-sms.gateway.ts`.
`PARSIAN_SMS_URL` is expected to already include the provider URL prefix and query key before receptor. The app appends `=<receptor>&Message=<encoded message>`.
```sh
PARSIAN_SMS_URL='https://apigateway.parsianinsurance.com/api/SendSMS?ReceiverNumbers'
PARSIAN_API_KEY='be988c9c-dbd6-494e-9c04-944ecc6426bf'
PARSIAN_BASIC_TOKEN='UGFyc2lhbkFQSTpQYXJzaWFuQHBpMjI='
RECEPTOR="09120000000"
MESSAGE="Hello"
curl -X GET "$PARSIAN_SMS_URL=$RECEPTOR&Message=$(printf %s "$MESSAGE" | jq -sRr @uri)" \
-H "Content-Type: application/json" \
-H "X-PACKAGE-API-KEY: $PARSIAN_API_KEY" \
-H "Authorization: Basic $PARSIAN_BASIC_TOKEN"
```
## Car Price Provider
Used by `src/expert-claim/expert-claim.service.ts` to fetch car prices from `CW_URL`.
```sh
CW_URL="<base URL ending with slash if required by provider>"
curl -X GET "${CW_URL}price?akharin"
curl -X GET "${CW_URL}price?hamrah"
```
## AI Service Calls Currently Disabled
`src/ai/ai.service.ts` contains configured URLs but the actual axios calls are commented out. If re-enabled, the sequence is:
1. `POST $AI_URL_V2/auth/login`
2. `GET $AI_URL_V2/auth/profile`
3. `POST $AI_URL_V2/services/car-damage/detector?version=ai-v7`
```sh
AI_URL_V2='https://ai-gw.ittalie.ir'
AI_USERNAME='yara@gmail.io'
AI_PASSWORD='123321'
curl -X POST "$AI_URL_V2/auth/login" \
-H "Content-Type: application/json" \
--data '{
"username": "'"$AI_USERNAME"'",
"password": "'"$AI_PASSWORD"'"
}'
AI_ACCESS_TOKEN="<response accessToken>"
curl -X GET "$AI_URL_V2/auth/profile" \
-H "Authorization: Bearer $AI_ACCESS_TOKEN"
GATEWAY_API_KEY="<profile apiKey.key>"
curl -X POST "$AI_URL_V2/services/car-damage/detector?version=ai-v7" \
-H "Authorization: Bearer $AI_ACCESS_TOKEN" \
-H "gateway-api-key: $GATEWAY_API_KEY" \
-F "images=@/path/to/car-image.jpg"
```
## Refresh Blame Inquiries Script
Used by `scripts/refresh-blame-inquiries.js`. This script does not login; it expects pre-provided bearer tokens:
- `TEJARAT_THIRD_PARTY_TOKEN` or `TEJARAT_TOKEN`
- `TEJARAT_CAR_BODY_TOKEN` or `TEJARAT_TOKEN`
- `TEJARAT_PERSON_TOKEN` or `TEJARAT_TOKEN`
Default URLs:
```sh
TEJARAT_THIRD_PARTY_URL="${TEJARAT_THIRD_PARTY_URL:-http://82.99.202.245:3027/block-inquiry-tejarat}"
TEJARAT_CAR_BODY_URL="${TEJARAT_CAR_BODY_URL:-http://82.99.202.245:3027/block-inquiry-tejarat/badane}"
TEJARAT_PERSON_URL="${TEJARAT_PERSON_URL:-http://82.99.202.245:3027/personal-inquiry/tejarat-no}"
```
Third-party inquiry:
```sh
curl -X POST "$TEJARAT_THIRD_PARTY_URL" \
-H "authorization: Bearer $TEJARAT_THIRD_PARTY_TOKEN" \
-H "content-type: application/json" \
-H "accept: application/json" \
--data '{
"leftTwoDigits": "12",
"serialLetter": "ب",
"threeDigits": "345",
"rightTwoDigits": "67",
"nationalCode": "0012345678"
}'
```
Car-body inquiry:
```sh
curl -X POST "$TEJARAT_CAR_BODY_URL" \
-H "authorization: Bearer $TEJARAT_CAR_BODY_TOKEN" \
-H "content-type: application/json" \
-H "accept: application/json" \
--data '{
"part1": 12,
"part2": "ب",
"part3": 345,
"part4": 67,
"nationalCode": "0012345678"
}'
```
Personal inquiry:
```sh
curl -X POST "$TEJARAT_PERSON_URL" \
-H "authorization: Bearer $TEJARAT_PERSON_TOKEN" \
-H "content-type: application/json" \
-H "accept: application/json" \
--data '{
"nationalCode": "0012345678",
"birthdate": "1999-02-13"
}'
```

View File

@@ -0,0 +1,596 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<title>مرجع یکپارچه‌سازی‌های خارجی</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Vazirmatn", "Tahoma", "Segoe UI", system-ui, sans-serif;
font-size: 14px; line-height: 1.8;
background: #ffffff; color: #1f2328; padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.7;
}
.card {
border: 1px solid #e5e7eb; border-radius: 6px;
padding: 16px; background: #f7f8fa; margin-bottom: 16px;
}
.card.card-blue { border-right: 4px solid #3b82f6; }
.card.card-green { border-right: 4px solid #22c55e; }
.card.card-purple { border-right: 4px solid #8b5cf6; }
.card.card-orange { border-right: 4px solid #f97316; }
.card.card-teal { border-right: 4px solid #14b8a6; }
.card.card-indigo { border-right: 4px solid #6366f1; }
.card.card-gray { border-right: 4px solid #94a3b8; }
.card.card-red { border-right: 4px solid #ef4444; }
.card.card-yellow { border-right: 4px solid #eab308; }
table {
border-collapse: collapse; width: 100%;
font-size: 12px; margin-top: 4px; direction: rtl;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: right; padding: 5px 8px; border: 1px solid #e5e7eb;
}
td { padding: 4px 8px; border: 1px solid #e5e7eb; vertical-align: top; }
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
direction: ltr; unicode-bidi: embed;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
direction: ltr; unicode-bidi: embed;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.note { font-size: 11px; color: #57606a; font-style: normal; margin-top: 6px; }
.warn { font-size: 11px; color: #9a3412; font-style: normal; margin-top: 6px; }
.status-badge {
display: inline-block; font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
}
.status-live { background: #dcfce7; color: #166534; }
.status-partial { background: #ffedd5; color: #9a3412; }
.status-disabled { background: #fee2e2; color: #991b1b; }
.status-internal { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px; margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-right: 18px; padding-left: 0; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb; margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
.flow-box {
background: #ffffff; border: 1px solid #e5e7eb; border-radius: 4px;
padding: 10px 14px; font-size: 12px; margin-top: 8px;
}
.flow-step {
display: flex; gap: 10px; align-items: flex-start; margin-bottom: 6px;
direction: rtl;
}
.flow-num {
flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%;
background: #3b82d4; color: #fff; font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.flow-text { flex: 1; padding-top: 2px; }
.decision-tree {
font-size: 12px; background: #ffffff;
border: 1px solid #e5e7eb; border-radius: 4px; padding: 12px 16px;
margin-top: 8px; line-height: 1.9;
}
.decision-tree ul { padding-right: 20px; padding-left: 0; }
.decision-tree li { margin-bottom: 2px; }
.env-table th:last-child { width: 220px; }
pre {
font-family: monospace; font-size: 11px;
background: #f1f5f9; border: 1px solid #e5e7eb;
border-radius: 4px; padding: 10px 12px;
white-space: pre-wrap; word-break: break-all;
margin-top: 6px; color: #1f2328;
direction: ltr; unicode-bidi: embed;
}
</style>
</head>
<body>
<div class="max-wrap">
<h1>مرجع یکپارچه‌سازی‌های خارجی</h1>
<p class="subtitle">
تمام یکپارچه‌سازی‌های خروجی: کاربرد، زمان فعال‌شدن، نحوه احراز هویت،
رفتار retry، fallbackها و تمام متغیرهای محیطی. سرویس‌های داخلی
(کپچا، داده‌های پرس‌وجوی آفلاین) برای کامل‌بودن گنجانده شده‌اند.
</p>
<!-- TOC -->
<div class="toc">
<div class="toc-title">فهرست</div>
<ol>
<li><a href="#inquiry-routing">درخت تصمیم مسیریابی پرس‌وجو</a></li>
<li><a href="#fanavaran">فناوران — پلتفرم خسارت بیمه</a></li>
<li><a href="#sanhub">SandHub — درگاه پرس‌وجوی قدیمی</a></li>
<li><a href="#tejarat">پرس‌وجوی تجارت — درگاه block-inquiry (V2+)</a></li>
<li><a href="#esg">ESG — ارائه‌دهنده پرس‌وجوی تنانت پارسیان</a></li>
<li><a href="#sms">پیامک — درگاه‌های کاوه‌نگار و پارسیان</a></li>
<li><a href="#ai">سرویس هوش مصنوعی — تشخیص خسارت خودرو</a></li>
<li><a href="#car-pricing">سرویس قیمت خودرو — جستجوی ارزش بازار</a></li>
<li><a href="#offline-inquiry">پرس‌وجوی آفلاین — داده‌های fallback</a></li>
<li><a href="#env-ref">مرجع متغیرهای محیطی</a></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="inquiry-routing">۱ — درخت تصمیم مسیریابی پرس‌وجو</h2>
<p class="section-intro">
هر فایل تقصیر با فراخوانی "run-inquiries" آغاز می‌شود که بیمه‌نامه طرف مقصر
را از یک ارائه‌دهنده خارجی دریافت می‌کند. اینکه کدام ارائه‌دهنده واقعاً فراخوانی
می‌شود به سه عامل بستگی دارد: تنانت (<code>CLIENT_ID</code>)، نوع فایل
(THIRD_PARTY در مقابل CAR_BODY) و اینکه آیا حالت API زنده در تنظیمات سیستم
فعال است یا خیر. لایه داده‌های پرس‌وجوی آفلاین در جلوی هر سه ارائه‌دهنده قرار دارد.
</p>
<div class="card card-indigo">
<h3>انتخاب ارائه‌دهنده</h3>
<div class="decision-tree">
<strong>برای هر پرس‌وجوی مبتنی بر پلاک:</strong>
<ul>
<li>۱. بررسی داده‌های آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → مسیریابی به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> یا <code>/inquiry/policyByChassis</code>.</li>
<li>۳. در غیر این صورت → مسیریابی به <strong>پرس‌وجوی تجارت</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) یا <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
<li>۴. اگر <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (پیش‌فرض) → پاسخ mock برگردانده شود به جای انجام فراخوانی‌های HTTP.</li>
</ul>
<br>
<strong>برای بررسی‌های هویت شخصی، گواهینامه، مالکیت و شبا:</strong>
<ul>
<li>اگر <code>CLIENT_ID=8</code> → ESG <code>/inquiry/person</code> و <code>/inquiry/sheba</code>.</li>
<li>در غیر این صورت → تجارت/SandHub <code>/personal-inquiry/tejarat-no</code>، <code>/driver-license-check</code>، <code>/ownership</code>، <code>/sheba/sheba-tejaratno</code>.</li>
</ul>
<br>
<strong>تفاوت کلیدی — فرمت تاریخ تولد:</strong>
SandHub/تجارت تاریخ تولد <em>میلادی</em> انتظار دارند (داخلی از جلالی تبدیل می‌شود).
ESG مستقیماً تاریخ <em>جلالی</em> انتظار دارد.
</div>
<p class="note" style="margin-top:8px;">
اندپوینت‌های SandHub فقط در مسیرهای قدیمی کد استفاده می‌شوند. تمام جریان‌های فعال تقصیر V2+ از طریق ارائه‌دهندگان تجارت یا ESG می‌روند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="fanavaran">۲ — فناوران <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
فناوران (<code>apimanager.iraneit.com</code>) پلتفرم ملی پرونده خسارت بیمه است.
پس از اینکه کارشناس خسارت ارزیابی خود را ارسال می‌کند، سیستم به‌صورت خودکار
یک خسارت ساختاریافته را از طریق یک پروتکل چهار مرحله‌ای به فناوران ارسال می‌کند.
فناوران همچنین به‌عنوان منبع جستجوی code-listها (انواع تصادف، اجزای خودرو،
کدهای شهر و غیره) در سراسر پلتفرم عمل می‌کند.
</p>
<div class="card card-blue">
<h3>چرخه حیات احراز هویت</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>GET AppToken</strong> — <code>POST /EITAuthentication/GetAppToken</code> با هدرهای <code>appname</code> + <code>secret</code>. هدر <code>apptoken</code> را برمی‌گرداند.</div></div>
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>Login</strong> — <code>POST /EITAuthentication/Login</code> با هدرهای <code>appToken</code> + <code>userName</code> + <code>password</code>. هدر <code>authenticationToken</code> را برمی‌گرداند.</div></div>
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>Cache</strong> — توکن در حافظه <em>و</em> پایدار در MongoDB (<code>fanavaran_auth_tokens</code>) ذخیره می‌شود. تا نیمه‌شب <strong>Asia/Tehran</strong> معتبر است — اولین فراخوانی پس از ۰۰:۰۰ یک توکن تازه دریافت می‌کند.</div></div>
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>تمام فراخوانی‌های بعدی</strong> چهار هدر شامل می‌شوند: <code>authenticationToken</code>، <code>CorpId</code>، <code>ContractId</code>، <code>Location</code> — مختص تنانت، hardcoded به ازای هر کلید <code>FANAVARAN_CLIENT</code>.</div></div>
</div>
<p class="note" style="margin-top:8px;">
یک اثر انگشت پیکربندی (هش appName + secret + username + password + corpId + contractId + location)
یک ورود تازه را زمانی که هر مدرکی تغییر کند، حتی قبل از نیمه‌شب، مجبور می‌کند.
</p>
</div>
<div class="card card-blue">
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>خسارت پایه (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. داده‌های مالک، راننده، بیمه، وسیله نقلیه و تصادف را ارسال می‌کند. یک <code>claimId</code> و <code>claimNo</code> فناوران برمی‌گرداند. پیامک با هر دو شناسه برای مالک ارسال می‌شود.</div></div>
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>موارد خسارت (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. یک ورودی به ازای هر قطعه آسیب‌دیده با شناسه کامپوننت، شدت و قیمت. سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان.</div></div>
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>پیوست‌ها (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. اسناد، تصاویر car-capture و ویدیوها که با شناسه فایل ارجاع داده شده‌اند.</div></div>
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>کارشناسی (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. متادیتای ارزیابی کارشناس (نقش کارشناس، تاریخ، نتیجه). ارسال را نهایی می‌کند.</div></div>
</div>
<p class="note" style="margin-top:8px;">
هر چهار مرحله در مجموعه <code>fanavaran_audit_logs</code> با بدنه کامل درخواست/پاسخ، وضعیت HTTP، مدت زمان و کد ردیابی برای اشکال‌زدایی ثبت می‌شوند.
</p>
</div>
<div class="card card-blue">
<h3>اندپوینت‌های Lookup</h3>
<p class="note">همه زیر <code>https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/</code>. نتایج روی دیسک (به ازای کلید مشتری) و در مجموعه MongoDB <code>lookups</code> کش می‌شوند. تنانت پارسیان قبل از درخواست API از DB می‌خواند؛ دیگران ابتدا به API می‌روند.</p>
<table>
<tr><th>کاربرد</th><th>مسیر</th></tr>
<tr><td>گزینه‌های dropdown برای accidentReason (نگاشت شده به شناسه‌های محلی)</td><td><code>/car/base-info/accident-causes</code></td></tr>
<tr><td>گزینه‌های accidentWay</td><td><code>/car/code-list/accident-report-type</code></td></tr>
<tr><td>طبقه‌بندی استفاده از وسیله نقلیه</td><td><code>/car/base-info/vehicle-use-types</code></td></tr>
<tr><td>روش پرداخت خسارت</td><td><code>/car/code-list/dmg-pay-method</code></td></tr>
<tr><td>گزینه‌های نوع گواهینامه</td><td><code>/car/base-info/driving-licence-types</code></td></tr>
<tr><td>طبقه‌بندی طرف مقصر</td><td><code>/car/code-list/accident-culprit-type</code></td></tr>
<tr><td>گزینه‌های محل بازرسی</td><td><code>/car/code-list/inspection-place</code></td></tr>
<tr><td>کدهای وضعیت کاهش قیمت</td><td><code>/car/code-list/drop-amount-status</code></td></tr>
<tr><td>کاتالوگ کامپوننت (نگاشت به قطعات بیرونی/داخلی)</td><td><code>/car/base-info/car-components</code></td></tr>
<tr><td>گزینه‌های شدت تصادف</td><td><code>/car/code-list/accident-level</code></td></tr>
<tr><td>تطبیق <code>INSURANCE_CORP_ID</code> ← corpId فناوران</td><td><code>/common/code-list/insurance-corp</code></td></tr>
<tr><td>انتخابگرهای شهر/استان</td><td><code>/common/base-info/cities</code>، <code>/common/base-info/Provinces</code></td></tr>
<tr><td>دریافت بیمه‌نامه کامل بر اساس شناسه پس از استعلام</td><td><code>/car/third-party-car-policies/{policyId}</code></td></tr>
<tr><td>جستجوی وسیله نقلیه بر اساس VIN</td><td><code>/car/vehicles/inquiry-by-vin?vin=…</code></td></tr>
<tr><td>فهرست بیمه‌نامه‌ها برای یک کد ملی</td><td><code>/common/Policies/inquiry-my-policies</code></td></tr>
<tr><td>دریافت رکورد مشتری بر اساس شناسه</td><td><code>/common/customers/{customerId}</code></td></tr>
<tr><td>جستجوی طرف بر اساس کد ملی + تاریخ تولد</td><td><code>/common/parties/inquiry-by-unique-identifier</code></td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت خطا و انعطاف‌پذیری</h3>
<table>
<tr><th>توضیح</th><th>مکانیزم</th></tr>
<tr><td>۳ تلاش، ۵۰۰ ms ← ۱۰۰۰ ms backoff نمایی در تمام فراخوانی‌های HTTP.</td><td>Retry</td></tr>
<tr><td>وقتی فناوران پیام فارسی "دوباره تلاش کنید" (یا tracking-code 500) برمی‌گرداند، یک مکث ۵ دقیقه‌ای در سطح تنانت فعال می‌شود. تمام فراخوانی‌ها در این پنجره بلافاصله <code>503 ServiceUnavailable</code> دریافت می‌کنند — بدون فشار.</td><td>Backoff گذرا</td></tr>
<tr><td>در ۴۰۱، توکن از حافظه و MongoDB پاک می‌شود؛ فراخوانی بعدی GetAppToken + Login تازه را فعال می‌کند.</td><td>ابطال توکن</td></tr>
<tr><td>درخواست‌های همزمان ورود برای همان تنانت به یک Promise در حال پرواز جمع می‌شوند.</td><td>حذف تکراری Inflight</td></tr>
<tr><td>هر مرحله (GET_APP_TOKEN، LOGIN و هر چهار مرحله ارسال) در <code>fanavaran_audit_logs</code> با وضعیت STARTED / SUCCESS / FAILURE، هدرهای کامل، بدنه و مدت زمان نوشته می‌شود.</td><td>لاگ Audit</td></tr>
<tr><td>۲۰–۳۰ ثانیه به ازای هر فراخوانی HTTP.</td><td>Timeout</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>پروفایل‌های تنانت (<code>FANAVARAN_CLIENT</code>)</h3>
<p class="section-intro" style="margin-top:6px; margin-bottom:8px;">سه پروفایل تنانت از پیش تعیین‌شده وجود دارد. پروفایل فعال توسط متغیر محیطی <code>FANAVARAN_CLIENT</code> انتخاب می‌شود. هر پروفایل <code>appName</code>، <code>secret</code>، <code>username</code>، <code>password</code>، <code>CorpId</code>، <code>ContractId</code> و <code>Location</code> هدرهای خود را به علاوه پیش‌فرض‌های payload (AccidentCityId و غیره) دارد.</p>
<table>
<tr><th>شرکت بیمه</th><th>کلید</th></tr>
<tr><td>بیمه پارسیان</td><td><code>parsian</code></td></tr>
<tr><td>بیمه تجارت نو</td><td><code>tejaratno</code></td></tr>
<tr><td>بیمه معلم</td><td><code>moallem</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">
<code>INSURANCE_CORP_ID</code> یک رشته عنوان نمایشی است (مثلاً <em>"بیمه پارسیان"</em>) که در برابر فهرست زنده فناوران <code>insurance-corp</code> تطبیق داده می‌شود تا <code>corpId</code> عددی مورد استفاده در ارسال‌ها را تولید کند. شناسه تطبیق‌یافته روی دیسک کش می‌شود.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sanhub">۳ — SandHub <span class="status-badge status-partial">قدیمی</span></h2>
<p class="section-intro">
SandHub درگاه پرس‌وجوی اصلی است. هنوز در کدبیس حضور دارد اما تمام جریان‌های
فعال تقصیر (V2+) به ارائه‌دهنده پرس‌وجوی تجارت منتقل شده‌اند. اندپوینت‌های
SandHub قابل فراخوانی هستند اما فقط از طریق مسیرهای قدیمی کد قابل دسترسی هستند.
حالت mock آن توسط همان تنظیم سیستم <code>sandHubUseLiveApi</code> کنترل می‌شود.
</p>
<div class="card card-gray">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {SANHUB_BASE_URL}/user/login</code> با بدنه JSON نام کاربری + رمز عبور.
توکن در حافظه برای <strong>۵۵ دقیقه</strong> کش می‌شود. در ۴۰۱، توکن پاک می‌شود و یک تلاش مجدد انجام می‌شود.
۳ تلاش با ۱۰۰۰ ms ← ۲۰۰۰ ms backoff نمایی.
</p>
</div>
<div class="card card-gray">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>پرس‌وجوی بیمه‌نامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>leftTwoDigits</code>، <code>serialLetter</code>، <code>threeDigits</code>، <code>rightTwoDigits</code>، <code>nationalCode</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>پرس‌وجوی بیمه‌نامه CAR_BODY. Timeout ۵۰ ثانیه (طولانی‌تر از استاندارد).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/personal-inquiry/tejarat-no</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code> + <code>birthDate</code> <em>میلادی</em> (داخلی از جلالی تبدیل می‌شود).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/driver-license-check</code></td><td>اعتبارسنجی گواهینامه. پرچم <code>IsSucceed</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/ownership</code></td><td>بررسی مالکیت وسیله نقلیه. پرچم <code>IsSuccess</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/sheba/sheba-tejaratno</code></td><td>اعتبارسنجی شبا / حساب بانکی. <code>ReturnValue</code> + <code>HasError</code> را برمی‌گرداند.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
تمام اندپوینت‌ها پاسخ‌های mock کامل را زمانی که <code>sandHubUseLiveApi=false</code> در تنظیمات سیستم (پیش‌فرض) پشتیبانی می‌کنند. داده‌های mock قطعی هستند و به‌صورت محلی بدون هیچ فراخوانی HTTP تولید می‌شوند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="tejarat">۴ — پرس‌وجوی تجارت <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
درگاه فعال block-inquiry برای تمام تنانت‌های غیر ESG. در هر فراخوانی V2+
<code>run-inquiries</code> که <code>CLIENT_ID ≠ 8</code> استفاده می‌شود.
URL پایه قابل پیکربندی است؛ در تولید به همان هاست SandHub اشاره می‌کند اما از
اعتبارنامه‌های جداگانه استفاده می‌کند.
</p>
<div class="card card-teal">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {TEJARAT_INQUIRY_BASE_URL}/user/login</code> با بدنه JSON ایمیل + رمز عبور.
توکن برای <strong>۵۵ دقیقه</strong> کش می‌شود. ۲ تلاش با ۵۰۰ ms ← ۱۰۰۰ ms backoff.
جدا از اعتبارنامه‌های SandHub — از <code>TEJARAT_INQUIRY_EMAIL</code> / <code>TEJARAT_INQUIRY_PASSWORD</code> استفاده می‌کند.
</p>
</div>
<div class="card card-teal">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>پرس‌وجوی پلاک THIRD_PARTY. بدنه: فیلدهای پلاک + <code>nationalCode</code>. ابتدا داده آفلاین بررسی می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>پرس‌وجوی پلاک CAR_BODY. بدنه: <code>part1–part4</code> (عددی) + <code>nationalCode</code>. همیشه زنده می‌شود (mock برای مسیر badane وجود ندارد).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
وقتی <code>sandHubUseLiveApi=false</code>، مسیر THIRD_PARTY یک پاسخ mock بدون HTTP برمی‌گرداند. مسیر CAR_BODY همیشه API زنده را صرف‌نظر از این پرچم فراخوانی می‌کند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="esg">۵ — ESG <span class="status-badge status-live">فعال (CLIENT_ID=8)</span></h2>
<p class="section-intro">
ESG یک درگاه API بیمه داخلی است که منحصراً توسط تنانت پارسیان
(<code>CLIENT_ID=8</code>) استفاده می‌شود. برای تمام انواع پرس‌وجو زمانی که
این تنانت فعال است، جایگزین تجارت/SandHub می‌شود. شکل پاسخ متفاوتی دارد،
TTL توکن پویا دارد و تاریخ تولد را در فرمت <strong>جلالی</strong> انتظار دارد
(نه میلادی، برخلاف SandHub/تجارت).
</p>
<div class="card card-purple">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {ESG_URL}/auth/login</code> با بدنه JSON <code>{ username, password }</code>.
TTL توکن از فیلد <code>expiresIn</code> پاسخ خوانده می‌شود (پیش‌فرض ۱۴ دقیقه).
۲ تلاش با ۵۰۰ ms ← ۱۰۰۰ ms backoff. در ۴۰۱، توکن پاک و یک تلاش مجدد.
URL پیش‌فرض: <code>http://192.168.20.22:8085</code> (شبکه داخلی).
</p>
</div>
<div class="card card-purple">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>جستجوی بیمه‌نامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>nationalCode</code>، <code>plk1–plk4</code>. پاسخ قبل از ذخیره به فرمت قدیمی تجارت نگاشت می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>جایگزین مبتنی بر VIN/شاسی برای پرس‌وجوی پلاک. توسط اندپوینت‌های <code>run-inquiries-vin</code> فراخوانی می‌شود. از جستجوی شاسی ESG استفاده می‌کند (نه مسیر SandHub). بدنه: <code>nationalCode</code>، <code>chassis</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code>، <code>birthDate</code> (جلالی، نه میلادی).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>اعتبارسنجی شبا / حساب بانکی.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
ESG هر پاسخ را به صورت <code>{ success: boolean, data: … }</code> می‌پیچد. یک بدنه <code>success=false</code> به یک خطای فارسی "استعلام در دسترس نیست" ترجمه می‌شود.
بررسی داده آفلاین-پرس‌وجو هنوز ابتدا اجرا می‌شود، قبل از هر فراخوانی HTTP ESG.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sms">۶ — پیامک <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
دو ارائه‌دهنده پیامک پشتیبانی می‌شوند: <strong>کاوه‌نگار</strong> (پیش‌فرض)
و <strong>درگاه پیامک پارسیان</strong>. ارائه‌دهنده فعال توسط متغیر محیطی
<code>SMS_PROVIDER</code> (یا <code>SMS</code>) انتخاب می‌شود. هر دو ارائه‌دهنده
رابط درگاه داخلی یکسانی را پیاده‌سازی می‌کنند بنابراین لایه ارکستراسیون
مستقل از ارائه‌دهنده است.
</p>
<div class="card card-green">
<h3>انتخاب ارائه‌دهنده</h3>
<table>
<tr><th>ارائه‌دهنده فعال</th><th>مقدار</th><th>متغیر محیطی</th></tr>
<tr><td>کاوه‌نگار — <code>api.kavenegar.com</code></td><td><code>kavenegar</code> (پیش‌فرض)</td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
<tr><td>درگاه پیامک پارسیان — <code>PARSIAN_SMS_URL</code></td><td><code>parsian</code></td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
</table>
</div>
<div class="card card-green">
<h3>اندپوینت‌های کاوه‌نگار</h3>
<p class="note">URL پایه: <code>https://api.kavenegar.com/v1/{SMS_API_KEY}/</code></p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>زمان استفاده</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>sms/send.json</code></td><td>پیام‌های متن ساده (مثلاً متن‌های اطلاع‌رسانی مبتنی بر کلید ذخیره‌شده در مجموعه <code>sms_texts</code>).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>verify/lookup.json</code></td><td>تمام پیام‌های مبتنی بر قالب (OTPها، لینک‌های دعوت، اطلاع‌رسانی کارشناس). پارامترها: <code>receptor</code>، <code>token</code>[، <code>token2</code>، <code>token3</code>، <code>token10</code>]، <code>template</code>.</td></tr>
</table>
</div>
<div class="card card-green">
<h3>درگاه پیامک پارسیان</h3>
<p class="note">URL پایه از <code>PARSIAN_SMS_URL</code>. احراز هویت: هدر <code>X-PACKAGE-API-KEY</code> + <code>Authorization: Basic {PARSIAN_BASIC_TOKEN}</code>. به‌صورت GET با پارامترهای URL-encoded <code>ReceiverNumbers</code> و <code>Message</code> ارسال می‌کند. پیام‌های قالب قبل از ارسال به یک بدنه متن ساده پیش‌رندر می‌شوند (معادل verify/lookup ندارد).</p>
</div>
<div class="card card-green">
<h3>قالب‌های پیامک در حال استفاده</h3>
<table>
<tr><th>توکن‌ها</th><th>ماشه</th><th>نام قالب</th></tr>
<tr><td><code>token</code> = کد OTP</td><td>ورود OTP کاربر / اکتور، فراموشی رمز، OTPهای طرف</td><td><code>AUTH_SMS_TEMPLATE</code> (محیطی)</td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>طرف دوم لینک دعوت تقصیر را از طریق پیامک دریافت می‌کند</td><td><code>yara724-invite-link</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = نام خانوادگی کارشناس، <code>token3</code> = لینک</td><td>کارشناس میدانی لینک را برای یک طرف ارسال می‌کند</td><td><code>yara-field-expert-link</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>اطلاع به طرف که طرف دیگر با رأی کارشناس موافقت کرده است</td><td><code>yara-blame-agreement</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>طرف زیان‌دیده مطلع می‌شود که جریان خسارت را پس از تکمیل تقصیر باز کند</td><td><code>yara-claim-link</code></td></tr>
<tr><td><code>token</code> = "تصادف"/"خسارت"، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس</td><td>کارشناس یک فایل تقصیر یا خسارت را قفل می‌کند</td><td><code>yara-expert-lock</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = لینک</td><td>کارشناس درخواست ارسال مجدد اسناد می‌دهد</td><td><code>yara-resend-documents</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس، <code>token10</code> = لینک</td><td>طرف مطلع می‌شود که ارزیابی خسارت کارشناس را امضا کند</td><td><code>yara-signature</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = claimId فناوران، <code>token3</code> = claimNo فناوران</td><td>ارسال فناوران تأیید شد — با شماره و شناسه خسارت فناوران برای مالک خسارت ارسال می‌شود</td><td><code>yara-fanavaran-claim</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">
تمام فراخوانی‌های پیامک fire-and-forget هستند — هرگز throw نمی‌کنند. شکست‌ها log می‌شوند اما جریان اصلی را مسدود نمی‌کنند.
یک مجموعه MongoDB <code>sms_send_logs</code> هر پیام خروجی را با نوع آن (OTP در مقابل TEMPLATE)، ارائه‌دهنده، نام قالب و وضعیت موفقیت/شکست ثبت می‌کند.
پیام‌های متنی اطلاع‌رسانی (اختلاف طرفین، امضای یک طرف و غیره) در راه‌اندازی در مجموعه <code>sms_texts</code> seed می‌شوند و در زمان اجرا قابل ویرایش هستند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="ai">۷ — سرویس هوش مصنوعی <span class="status-badge status-disabled">غیرفعال (کد موجود)</span></h2>
<p class="section-intro">
یک سرویس تشخیص خسارت خودرو مبتنی بر تصویر در کدبیس یکپارچه‌سازی شده است اما
فراخوانی‌های HTTP آن <strong>کاملاً comment شده‌اند</strong>. ماژول در راه‌اندازی
مقداردهی اولیه می‌شود، تلاش برای ورود می‌کند (در صورت شکست به صورت خاموش
بلعیده می‌شود)، و یک متد <code>aiRequestImage</code> را expose می‌کند — اما
فراخوانی‌های axios زیرین غیرفعال هستند. سرویس هیچ جریان تولیدی را تحت تأثیر
قرار نمی‌دهد.
</p>
<div class="card card-yellow">
<h3>رابط مورد نظر (زمانی که دوباره فعال شود)</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/auth/login</code></td><td>احراز هویت با نام کاربری + رمز عبور. <code>accessToken</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{AI_URL_V2}/auth/profile</code></td><td>دریافت <code>apiKey.key</code> مورد نیاز به‌عنوان هدر درخواست <code>gateway-api-key</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/services/car-damage/detector?version=ai-v7</code></td><td>ارسال تصویر قطعه خودرو (multipart). <code>downloadLink</code> با نتیجه حاشیه‌نویسی‌شده را برمی‌گرداند.</td></tr>
</table>
<p class="warn" style="margin-top:8px;">
وضعیت: هر سه فراخوانی در بلوک‌های <code>axios.request(…)</code> comment-شده پیچیده شده‌اند.
<code>CW_URL</code> در <code>.env.example</code> نیست. برای فعال‌سازی مجدد، فراخوانی‌های axios login، getApiKey و aiRequestImage را uncomment کنید و <code>AI_URL_V2</code>، <code>AI_USERNAME</code>، <code>AI_PASSWORD</code> را پیکربندی کنید.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="car-pricing">۸ — سرویس قیمت خودرو <span class="status-badge status-partial">نیمه‌فعال</span></h2>
<p class="section-intro">
فقط در طول محاسبه کاهش قیمت کارشناس-خسارت استفاده می‌شود. وقتی یک کارشناس
مقادیر شدت برای هر قطعه ارائه می‌دهد، سیستم قیمت‌های بازار بلادرنگ برای مدل
خودروی آسیب‌دیده را دریافت می‌کند، سپس کاهش قیمت را با استفاده از فرمول
محاسبه می‌کند: <strong>قیمت خودرو × ضریب سال × مجموع ضرایب قطعات ÷ ۴۰۰</strong>.
سرویس دو منبع داده (اندپوینت) دارد که به‌صورت موازی امتحان می‌شوند.
</p>
<div class="card card-orange">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?akharin</code></td><td>دریافت قیمت‌های بازار خودرو از منبع "آخرین". آرایه <code>{ carName, marketPrice }</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?hamrah</code></td><td>دریافت قیمت‌های بازار خودرو از منبع "همراه". همان شکل پاسخ.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
هر دو اندپوینت امتحان می‌شوند؛ نتایج ادغام و حذف تکراری می‌شوند. بهترین تطابق برای
نام خودروی آسیب‌دیده با استفاده از <strong>فاصله Levenshtein</strong> (تطابق رشته فازی) پیدا می‌شود.
اگر هر دو اندپوینت شکست بخورند یا خالی برگردانند، محاسبه کاهش قیمت رد می‌شود (ناقص علامت‌گذاری می‌شود) — ارسال خسارت را مسدود نمی‌کند.
</p>
<p class="warn" style="margin-top:6px;">
<strong><code>CW_URL</code> در <code>.env.example</code> مستندسازی نشده است.</strong>
این سرویس در صورت تنظیم نشدن متغیر، به‌صورت خاموش هیچ کاهش قیمتی تولید نخواهد کرد.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="offline-inquiry">۹ — پرس‌وجوی آفلاین <span class="status-badge status-internal">داخلی / fallback</span></h2>
<p class="section-intro">
لایه پرس‌وجوی آفلاین فراخوانی‌های پرس‌وجوی مبتنی بر پلاک را قبل از اینکه هر
HTTP خارجی انجام شود رهگیری می‌کند. عمدتاً برای توسعه و تست (پلاک‌های شناخته‌شده
از پیش seed شده) استفاده می‌شود اما همچنین به‌عنوان fallback انعطاف‌پذیری
زمانی که سرویس‌های پرس‌وجوی زنده در دسترس نیستند عمل می‌کند. توسط یک پرچم
پایگاه‌داده زمان اجرا کنترل می‌شود، نه یک متغیر محیطی.
</p>
<div class="card card-gray">
<h3>نحوه کار</h3>
<table>
<tr><th>جزئیات</th><th>جنبه</th></tr>
<tr><td>مجموعه MongoDB <code>offline-inquiries</code>. اسناد شامل <code>clientKey</code>، فیلدهای نرمال‌شده پلاک، <code>nationalCode</code> و پاسخ از پیش ساخته‌شده <code>raw</code> + <code>mapped</code> برای برگرداندن هستند.</td><td>ذخیره‌سازی</td></tr>
<tr><td><code>system_settings.offlineInquiry.enabled</code> — پیش‌فرض <code>true</code>. تغییر از طریق <code>PATCH /super-admin/system-settings/offline-inquiry</code>.</td><td>سوئیچ اصلی</td></tr>
<tr><td>پلاک نرمال‌شده (فقط ارقام، عربی→فارسی) + کد ملی + کلید مشتری فناوران باید همه مطابقت داشته باشند. اگر پیدا شد، بلافاصله برگردانده می‌شود؛ هیچ فراخوانی HTTP انجام نمی‌شود.</td><td>ترتیب جستجو</td></tr>
<tr><td>فقط برای پرس‌وجوی block مبتنی بر پلاک (THIRD_PARTY) اعمال می‌شود. پرس‌وجوی CAR_BODY (<code>/badane</code>) همیشه API زنده را می‌زند.</td><td>محدوده</td></tr>
<tr><td><code>system_settings.externalApis.sandHubUseLiveApi</code> — وقتی <code>false</code> (پیش‌فرض)، حتی اگر هیچ داده آفلاینی مطابقت نداشته باشد، یک پاسخ mock داخلی برگردانده می‌شود به جای فراخوانی تجارت/ESG.</td><td>پرچم API زنده</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="env-ref">۱۰ — مرجع متغیرهای محیطی</h2>
<p class="section-intro">
تمام متغیرهای محیطی در سراسر تمام یکپارچه‌سازی‌ها، گروه‌بندی‌شده بر اساس سرویس.
متغیرهای علامت‌گذاری‌شده با <strong>*</strong> در <code>.env.example</code> وجود ندارند.
</p>
<div class="card card-blue">
<h3>فناوران</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>کلید پروفایل تنانت فعال: <code>parsian</code> | <code>tejaratno</code> | <code>moallem</code></td><td><code>FANAVARAN_CLIENT</code></td></tr>
<tr><td>عنوان نمایشی شرکت بیمه‌گر (مثلاً <em>"بیمه پارسیان"</em>) — در راه‌اندازی در برابر فهرست insurance-corp فناوران به یک corpId عددی تطبیق داده می‌شود.</td><td><code>INSURANCE_CORP_ID</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">اعتبارنامه‌های هر تنانت (appName، secret، username، password، CorpId، ContractId، Location) در <code>src/core/config/fanavaran-client.config.ts</code> زیر <code>SEED_FANAVARAN_CLIENT_PROFILES</code> hardcoded شده‌اند.</p>
</div>
<div class="card card-gray">
<h3>SandHub (قدیمی)</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه برای SandHub. پیش‌فرض: <code>http://82.99.202.245:3027</code></td><td><code>SANHUB_BASE_URL</code></td></tr>
<tr><td>URL کامل ورود (معمولاً base + <code>/user/login</code>)</td><td><code>SANHUB_URL_LOGIN</code></td></tr>
<tr><td>ایمیل ورود SandHub</td><td><code>SANHUB_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود SandHub</td><td><code>SANHUB_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-teal">
<h3>پرس‌وجوی تجارت</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه. پیش‌فرض: <code>http://82.99.202.245:3027</code></td><td><code>TEJARAT_INQUIRY_BASE_URL</code></td></tr>
<tr><td>ایمیل ورود</td><td><code>TEJARAT_INQUIRY_EMAIL</code></td></tr>
<tr><td>رمز عبور ورود</td><td><code>TEJARAT_INQUIRY_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-purple">
<h3>ESG (فقط CLIENT_ID=8)</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>به <code>8</code> تنظیم کنید تا ارائه‌دهنده پرس‌وجوی ESG برای تنانت پارسیان فعال شود.</td><td><code>CLIENT_ID</code></td></tr>
<tr><td>URL پایه ESG. پیش‌فرض: <code>http://192.168.20.22:8085</code> (شبکه داخلی)</td><td><code>ESG_URL</code></td></tr>
<tr><td>نام کاربری ورود ESG</td><td><code>ESG_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود ESG</td><td><code>ESG_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-green">
<h3>پیامک</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td><code>kavenegar</code> (پیش‌فرض) یا <code>parsian</code></td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
<tr><td>کلید API کاوه‌نگار (الزامی وقتی provider = kavenegar)</td><td><code>SMS_API_KEY</code></td></tr>
<tr><td>نام قالب کاوه‌نگار برای پیام‌های OTP (مثلاً <code>yara-otp</code>)</td><td><code>AUTH_SMS_TEMPLATE</code></td></tr>
<tr><td>URL پایه درگاه پیامک پارسیان (الزامی وقتی provider = parsian)</td><td><code>PARSIAN_SMS_URL</code></td></tr>
<tr><td>مقدار هدر پیامک پارسیان <code>X-PACKAGE-API-KEY</code></td><td><code>PARSIAN_API_KEY</code></td></tr>
<tr><td>اعتبارنامه‌های رمزگذاری‌شده Base64 برای هدر <code>Authorization: Basic …</code></td><td><code>PARSIAN_BASIC_TOKEN</code></td></tr>
<tr><td>URL پایه فرانت‌اند — برای ساخت تمام لینک‌های دعوت + خسارت تعبیه‌شده در پیام‌های پیامک استفاده می‌شود</td><td><code>URL</code></td></tr>
</table>
</div>
<div class="card card-yellow">
<h3>سرویس هوش مصنوعی</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه درگاه هوش مصنوعی. پیش‌فرض: <code>https://ai-gw.ittalie.ir</code> (استفاده نشده — سرویس غیرفعال است)</td><td><code>AI_URL_V2</code></td></tr>
<tr><td>نام کاربری ورود سرویس هوش مصنوعی (استفاده نشده)</td><td><code>AI_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود سرویس هوش مصنوعی (استفاده نشده)</td><td><code>AI_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-orange">
<h3>سرویس قیمت خودرو</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه برای API قیمت بازار خودرو (مثلاً <code>https://…/</code>). در <code>.env.example</code> نیست. کاهش قیمت به‌صورت خاموش رد می‌شود اگر تنظیم نشده باشد.</td><td><code>CW_URL</code> *</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>عمومی / برنامه</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>پورت HTTP (پیش‌فرض ۳۰۰۰). توسط fallback insurance-corp فناوران برای فراخوانی اندپوینت جستجوی محلی خودش استفاده می‌شود.</td><td><code>PORT</code></td></tr>
<tr><td><code>true</code> / <code>false</code> — چالش کپچای ورود را فعال/غیرفعال می‌کند. داخلی، بدون سرویس خارجی.</td><td><code>CAPTCHA_ENABLED</code></td></tr>
<tr><td>TTL چالش کپچا به دقیقه.</td><td><code>EXP_CAPTCHA_TIME</code></td></tr>
<tr><td>TTL کد یکبار مصرف به دقیقه.</td><td><code>EXP_OTP_TIME</code></td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,593 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>External Integrations Reference</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "Segoe UI", system-ui, sans-serif;
font-size: 14px; line-height: 1.6;
background: #ffffff; color: #1f2328; padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.5;
}
.card {
border: 1px solid #e5e7eb; border-radius: 6px;
padding: 16px; background: #f7f8fa; margin-bottom: 16px;
}
.card.card-blue { border-left: 4px solid #3b82f6; }
.card.card-green { border-left: 4px solid #22c55e; }
.card.card-purple { border-left: 4px solid #8b5cf6; }
.card.card-orange { border-left: 4px solid #f97316; }
.card.card-teal { border-left: 4px solid #14b8a6; }
.card.card-indigo { border-left: 4px solid #6366f1; }
.card.card-gray { border-left: 4px solid #94a3b8; }
.card.card-red { border-left: 4px solid #ef4444; }
.card.card-yellow { border-left: 4px solid #eab308; }
table {
border-collapse: collapse; width: 100%;
font-size: 12px; margin-top: 4px;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: left; padding: 5px 8px; border: 1px solid #e5e7eb;
}
td { padding: 4px 8px; border: 1px solid #e5e7eb; vertical-align: top; }
tr:nth-child(even) td { background: #ffffff; }
code { font-family: monospace; font-size: 11px; color: #3b82d4; }
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.note { font-size: 11px; color: #57606a; font-style: italic; margin-top: 6px; }
.warn { font-size: 11px; color: #9a3412; font-style: italic; margin-top: 6px; }
.status-badge {
display: inline-block; font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
}
.status-live { background: #dcfce7; color: #166534; }
.status-partial { background: #ffedd5; color: #9a3412; }
.status-disabled { background: #fee2e2; color: #991b1b; }
.status-internal { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px; margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-left: 18px; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb; margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
.flow-box {
background: #ffffff; border: 1px solid #e5e7eb; border-radius: 4px;
padding: 10px 14px; font-size: 12px; margin-top: 8px;
}
.flow-step {
display: flex; gap: 10px; align-items: flex-start; margin-bottom: 6px;
}
.flow-num {
flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%;
background: #3b82d4; color: #fff; font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.flow-text { flex: 1; padding-top: 2px; }
.decision-tree {
font-size: 12px; background: #ffffff;
border: 1px solid #e5e7eb; border-radius: 4px; padding: 12px 16px;
margin-top: 8px; line-height: 1.8;
}
.decision-tree ul { padding-left: 20px; }
.decision-tree li { margin-bottom: 2px; }
.env-table th:first-child { width: 220px; }
pre {
font-family: monospace; font-size: 11px;
background: #f1f5f9; border: 1px solid #e5e7eb;
border-radius: 4px; padding: 10px 12px;
white-space: pre-wrap; word-break: break-all;
margin-top: 6px; color: #1f2328;
}
</style>
</head>
<body>
<div class="max-wrap">
<h1>External Integrations Reference</h1>
<p class="subtitle">
Every outbound integration: what it does, when it fires, how auth works,
retry behaviour, fallbacks, and all environment variables. Internal-only
services (captcha, offline inquiry seed) are included for completeness.
</p>
<!-- TOC -->
<div class="toc">
<div class="toc-title">Contents</div>
<ol>
<li><a href="#inquiry-routing">Inquiry routing decision tree</a></li>
<li><a href="#fanavaran">Fanavaran — insurance claims platform</a></li>
<li><a href="#sanhub">SandHub — legacy inquiry gateway</a></li>
<li><a href="#tejarat">Tejarat inquiry — block-inquiry gateway (V2+)</a></li>
<li><a href="#esg">ESG — Parsian-tenant inquiry provider</a></li>
<li><a href="#sms">SMS — Kavenegar and Parsian gateways</a></li>
<li><a href="#ai">AI service — car damage detection</a></li>
<li><a href="#car-pricing">Car pricing service — market value lookup</a></li>
<li><a href="#offline-inquiry">Offline inquiry — fallback seed data</a></li>
<li><a href="#env-ref">Environment variable reference</a></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="inquiry-routing">1 — Inquiry Routing Decision Tree</h2>
<p class="section-intro">
Every blame file starts with a "run-inquiries" call that fetches the
guilty party's insurance policy from an external provider. Which provider
is actually called depends on three factors: the tenant (<code>CLIENT_ID</code>),
the file type (THIRD_PARTY vs CAR_BODY), and whether live API mode is
enabled in system settings. The offline-inquiry seed layer sits in front
of all three providers.
</p>
<div class="card card-indigo">
<h3>Provider selection</h3>
<div class="decision-tree">
<strong>For every plate-based block inquiry:</strong>
<ul>
<li>1. Check offline-inquiry seeds (MongoDB) — if a matching seed exists, return it and skip all HTTP.</li>
<li>2. If <code>CLIENT_ID=8</code> (Parsian/ESG tenant) → route to <strong>ESG</strong> <code>/inquiry/policyByPlate</code> or <code>/inquiry/policyByChassis</code>.</li>
<li>3. Otherwise → route to <strong>Tejarat inquiry</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) or <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
<li>4. If <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (default) → return mock response instead of making HTTP calls.</li>
</ul>
<br>
<strong>For personal-identity, driving-licence, ownership, and Sheba checks:</strong>
<ul>
<li>If <code>CLIENT_ID=8</code> → ESG <code>/inquiry/person</code> and <code>/inquiry/sheba</code>.</li>
<li>Otherwise → Tejarat/SandHub <code>/personal-inquiry/tejarat-no</code>, <code>/driver-license-check</code>, <code>/ownership</code>, <code>/sheba/sheba-tejaratno</code>.</li>
</ul>
<br>
<strong>Key difference — birth date format:</strong>
SandHub/Tejarat expect a <em>Gregorian</em> birth date (converted internally from Jalali).
ESG expects the <em>Jalali</em> date directly.
</div>
<p class="note" style="margin-top:8px;">
SandHub endpoints are only used in legacy code paths. All active V2+ blame flows go through the Tejarat or ESG providers.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="fanavaran">2 — Fanavaran <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
Fanavaran (<code>apimanager.iraneit.com</code>) is the national insurance
damage-case platform. After the damage expert submits their assessment,
the system auto-submits a structured claim to Fanavaran through a
four-step protocol. Fanavaran also serves as the lookup source for
code-lists (accident types, car components, city codes, etc.) used
across the platform.
</p>
<div class="card card-blue">
<h3>Authentication lifecycle</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>GET AppToken</strong> — <code>POST /EITAuthentication/GetAppToken</code> with <code>appname</code> + <code>secret</code> headers. Returns <code>apptoken</code> header.</div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Login</strong> — <code>POST /EITAuthentication/Login</code> with <code>appToken</code> + <code>userName</code> + <code>password</code> headers. Returns <code>authenticationToken</code> header.</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Cache</strong> — token is cached in memory <em>and</em> persisted to MongoDB (<code>fanavaran_auth_tokens</code>). Valid until midnight <strong>Asia/Tehran</strong> — the first call after 00:00 fetches a fresh token.</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>All subsequent calls</strong> include four headers: <code>authenticationToken</code>, <code>CorpId</code>, <code>ContractId</code>, <code>Location</code> — tenant-specific, hardcoded per <code>FANAVARAN_CLIENT</code> key.</div></div>
</div>
<p class="note" style="margin-top:8px;">
A config fingerprint (hash of appName + secret + username + password + corpId + contractId + location)
forces a fresh login when any credential changes, even before midnight.
</p>
</div>
<div class="card card-blue">
<h3>Claim submission protocol (4 steps)</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>Base claim (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. Sends owner, driver, insurance, vehicle, and accident data. Returns a Fanavaran <code>claimId</code> and <code>claimNo</code>. SMS is sent to the owner with both identifiers.</div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Damage cases (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. One entry per damaged part with component ID, severity, and price. Cap: total ≤ 53 000 000 Toman.</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Attachments (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. Documents, car-capture images, and videos referenced by file ID.</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>Expertise (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. Expert assessment metadata (expert role, date, result). Finalises the submission.</div></div>
</div>
<p class="note" style="margin-top:8px;">
All four steps are recorded in the <code>fanavaran_audit_logs</code> collection with full request/response bodies, HTTP status, duration, and tracking code for debugging.
</p>
</div>
<div class="card card-blue">
<h3>Lookup endpoints</h3>
<p class="note">All under <code>https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/</code>. Results are cached to disk (per client key) and in the <code>lookups</code> MongoDB collection. Parsian tenant reads DB before hitting the API; others go to the API first.</p>
<table>
<tr><th>Path</th><th>Used for</th></tr>
<tr><td><code>/car/base-info/accident-causes</code></td><td>accidentReason dropdown options (mapped to local IDs)</td></tr>
<tr><td><code>/car/code-list/accident-report-type</code></td><td>accidentWay options</td></tr>
<tr><td><code>/car/base-info/vehicle-use-types</code></td><td>vehicle usage classification</td></tr>
<tr><td><code>/car/code-list/dmg-pay-method</code></td><td>damage payment method</td></tr>
<tr><td><code>/car/base-info/driving-licence-types</code></td><td>licence type options</td></tr>
<tr><td><code>/car/code-list/accident-culprit-type</code></td><td>guilty-party classification</td></tr>
<tr><td><code>/car/code-list/inspection-place</code></td><td>inspection location options</td></tr>
<tr><td><code>/car/code-list/drop-amount-status</code></td><td>price-drop status codes</td></tr>
<tr><td><code>/car/base-info/car-components</code></td><td>component catalog (maps to outer/inner parts)</td></tr>
<tr><td><code>/car/code-list/accident-level</code></td><td>accident severity options</td></tr>
<tr><td><code>/common/code-list/insurance-corp</code></td><td>resolve <code>INSURANCE_CORP_ID</code> → Fanavaran corpId</td></tr>
<tr><td><code>/common/base-info/cities</code>, <code>/common/base-info/Provinces</code></td><td>city/province pickers</td></tr>
<tr><td><code>/car/third-party-car-policies/{policyId}</code></td><td>fetch full policy by ID after inquiry</td></tr>
<tr><td><code>/car/vehicles/inquiry-by-vin?vin=…</code></td><td>VIN-based vehicle lookup</td></tr>
<tr><td><code>/common/Policies/inquiry-my-policies</code></td><td>list policies for a national code</td></tr>
<tr><td><code>/common/customers/{customerId}</code></td><td>fetch customer record by ID</td></tr>
<tr><td><code>/common/parties/inquiry-by-unique-identifier</code></td><td>party lookup by national code + birth date</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Error handling &amp; resilience</h3>
<table>
<tr><th>Mechanism</th><th>Detail</th></tr>
<tr><td>Retry</td><td>3 attempts, 500 ms → 1 000 ms exponential backoff on all HTTP calls.</td></tr>
<tr><td>Transient backoff</td><td>When Fanavaran returns the Persian "try again later" message (or tracking-code 500), a tenant-wide 5-minute pause is activated. All calls during this window get <code>503 ServiceUnavailable</code> immediately — no hammering.</td></tr>
<tr><td>Token invalidation</td><td>On 401, token is cleared from memory and MongoDB; next call triggers a fresh GetAppToken + Login.</td></tr>
<tr><td>Inflight de-dup</td><td>Concurrent login requests for the same tenant are collapsed to a single in-flight Promise.</td></tr>
<tr><td>Audit log</td><td>Every step (GET_APP_TOKEN, LOGIN, and all four submission steps) is written to <code>fanavaran_audit_logs</code> with STARTED / SUCCESS / FAILURE status, full headers, body, and duration.</td></tr>
<tr><td>Timeout</td><td>20–30 s per HTTP call.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Tenant profiles (<code>FANAVARAN_CLIENT</code>)</h3>
<p class="section-intro" style="margin-top:6px; margin-bottom:8px;">Three pre-seeded tenant profiles exist. The active one is chosen by the <code>FANAVARAN_CLIENT</code> env var. Each profile carries its own <code>appName</code>, <code>secret</code>, <code>username</code>, <code>password</code>, <code>CorpId</code>, <code>ContractId</code>, and <code>Location</code> headers, plus payload defaults (AccidentCityId, etc.).</p>
<table>
<tr><th>Key</th><th>Insurance company</th></tr>
<tr><td><code>parsian</code></td><td>Parsian Insurance</td></tr>
<tr><td><code>tejaratno</code></td><td>Tejaratno Insurance</td></tr>
<tr><td><code>moallem</code></td><td>Moallem Insurance</td></tr>
</table>
<p class="note" style="margin-top:8px;">
<code>INSURANCE_CORP_ID</code> is a display-caption string (e.g. <em>"بیمه پارسیان"</em>) that is resolved against the live Fanavaran <code>insurance-corp</code> list to produce the numeric <code>corpId</code> used in submissions. The resolved ID is cached to disk.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sanhub">3 — SandHub <span class="status-badge status-partial">legacy</span></h2>
<p class="section-intro">
SandHub is the original inquiry gateway. It is still present in the
codebase but all active blame flows (V2+) have been migrated to the
Tejarat inquiry provider. SandHub endpoints remain callable but are
only reached through legacy code paths. Its mock mode is controlled
by the same <code>sandHubUseLiveApi</code> system setting.
</p>
<div class="card card-gray">
<h3>Auth</h3>
<p class="note">
<code>POST {SANHUB_BASE_URL}/user/login</code> with username + password JSON body.
Token cached in memory for <strong>55 minutes</strong>. On 401, token is cleared and one retry is made.
3 attempts with 1 000 ms → 2 000 ms exponential backoff.
</p>
</div>
<div class="card card-gray">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>Plate-based insurance policy inquiry (THIRD_PARTY). Body: <code>leftTwoDigits</code>, <code>serialLetter</code>, <code>threeDigits</code>, <code>rightTwoDigits</code>, <code>nationalCode</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>CAR_BODY policy inquiry. Timeout 50 s (longer than standard).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/personal-inquiry/tejarat-no</code></td><td>Personal identity check. Body: <code>nationalCode</code> + <em>Gregorian</em> <code>birthDate</code> (converted from Jalali internally).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/driver-license-check</code></td><td>Driving licence validation. Returns <code>IsSucceed</code> flag.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/ownership</code></td><td>Vehicle ownership check. Returns <code>IsSuccess</code> flag.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/sheba/sheba-tejaratno</code></td><td>Sheba / bank account validation. Returns <code>ReturnValue</code> + <code>HasError</code>.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
All endpoints support full mock responses when <code>sandHubUseLiveApi=false</code> in system settings (default). Mock data is deterministic and produced locally without any HTTP calls.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="tejarat">4 — Tejarat Inquiry <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
The active block-inquiry gateway for all non-ESG tenants. Used in every
V2+ <code>run-inquiries</code> call where <code>CLIENT_ID ≠ 8</code>.
The base URL is configurable; in production it points to the same host
as SandHub but uses separate credentials.
</p>
<div class="card card-teal">
<h3>Auth</h3>
<p class="note">
<code>POST {TEJARAT_INQUIRY_BASE_URL}/user/login</code> with email + password JSON body.
Token cached for <strong>55 minutes</strong>. 2 attempts with 500 ms → 1 000 ms backoff.
Separate from SandHub credentials — uses <code>TEJARAT_INQUIRY_EMAIL</code> / <code>TEJARAT_INQUIRY_PASSWORD</code>.
</p>
</div>
<div class="card card-teal">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>THIRD_PARTY plate inquiry. Body: plate fields + <code>nationalCode</code>. Offline seed checked first.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>CAR_BODY plate inquiry. Body: <code>part1–part4</code> (numeric) + <code>nationalCode</code>. Always goes live (no mock for badane path).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
When <code>sandHubUseLiveApi=false</code>, the THIRD_PARTY path returns a mock response without HTTP. The CAR_BODY path always calls the live API regardless of this flag.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="esg">5 — ESG <span class="status-badge status-live">live (CLIENT_ID=8)</span></h2>
<p class="section-intro">
ESG is an internal insurance API gateway used exclusively by the Parsian
tenant (<code>CLIENT_ID=8</code>). It replaces Tejarat/SandHub for all
inquiry types when this tenant is active. It has a different response
shape, a dynamic token TTL, and expects birth dates in <strong>Jalali</strong>
format (not Gregorian, unlike SandHub/Tejarat).
</p>
<div class="card card-purple">
<h3>Auth</h3>
<p class="note">
<code>POST {ESG_URL}/auth/login</code> with <code>{ username, password }</code> JSON body.
Token TTL is read from the response <code>expiresIn</code> field (default 14 min).
2 attempts with 500 ms → 1 000 ms backoff. On 401, token cleared and one retry.
Default URL: <code>http://192.168.20.22:8085</code> (internal network).
</p>
</div>
<div class="card card-purple">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>Plate-based policy lookup (THIRD_PARTY). Body: <code>nationalCode</code>, <code>plk1–plk4</code>. Response is mapped to the old Tejarat format before being stored.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>VIN/chassis-based alternative to plate inquiry. Called by <code>run-inquiries-vin</code> endpoints. Uses ESG chassis lookup (not the SandHub path). Body: <code>nationalCode</code>, <code>chassis</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>Personal identity check. Body: <code>nationalCode</code>, <code>birthDate</code> (Jalali, NOT Gregorian).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>Sheba / bank account validation.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
ESG wraps every response as <code>{ success: boolean, data: … }</code>. A <code>success=false</code> body is translated to a Persian "استعلام در دسترس نیست" (inquiry unavailable) error.
The offline-inquiry seed check still runs first, before any ESG HTTP call.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sms">6 — SMS <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
Two SMS providers are supported: <strong>Kavenegar</strong> (default)
and <strong>Parsian SMS Gateway</strong>. The active provider is chosen
by the <code>SMS_PROVIDER</code> (or <code>SMS</code>) env var. Both
providers implement the same internal gateway interface so the
orchestration layer is provider-agnostic.
</p>
<div class="card card-green">
<h3>Provider selection</h3>
<table>
<tr><th>Env var</th><th>Value</th><th>Active provider</th></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>kavenegar</code> (default)</td><td>Kavenegar — <code>api.kavenegar.com</code></td></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>parsian</code></td><td>Parsian SMS Gateway — <code>PARSIAN_SMS_URL</code></td></tr>
</table>
</div>
<div class="card card-green">
<h3>Kavenegar endpoints</h3>
<p class="note">Base URL: <code>https://api.kavenegar.com/v1/{SMS_API_KEY}/</code></p>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>When used</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>sms/send.json</code></td><td>Plain-text messages (e.g. key-based notification texts stored in <code>sms_texts</code> collection).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>verify/lookup.json</code></td><td>All template-based messages (OTPs, invite links, expert notifications). Params: <code>receptor</code>, <code>token</code>[, <code>token2</code>, <code>token3</code>, <code>token10</code>], <code>template</code>.</td></tr>
</table>
</div>
<div class="card card-green">
<h3>Parsian SMS Gateway</h3>
<p class="note">Base URL from <code>PARSIAN_SMS_URL</code>. Auth: <code>X-PACKAGE-API-KEY</code> header + <code>Authorization: Basic {PARSIAN_BASIC_TOKEN}</code>. Sends as a GET with URL-encoded <code>ReceiverNumbers</code> and <code>Message</code> query params. Template messages are pre-rendered into a plain text body before sending (no verify/lookup equivalent).</p>
</div>
<div class="card card-green">
<h3>SMS templates in use</h3>
<table>
<tr><th>Template name</th><th>Trigger</th><th>Tokens</th></tr>
<tr><td><code>AUTH_SMS_TEMPLATE</code> (env)</td><td>User / actor OTP login, forget-password, party OTPs</td><td><code>token</code> = OTP code</td></tr>
<tr><td><code>yara724-invite-link</code></td><td>Second party receives blame invite link via SMS</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-field-expert-link</code></td><td>Field expert sends link to a party</td><td><code>token</code> = file type, <code>token2</code> = expert surname, <code>token3</code> = link</td></tr>
<tr><td><code>yara-blame-agreement</code></td><td>Notify party that the other side agreed to the expert verdict</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-claim-link</code></td><td>Damaged party notified to open claim flow after blame is complete</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-expert-lock</code></td><td>Expert locks a blame or claim file</td><td><code>token</code> = "تصادف"/"خسارت", <code>token2</code> = publicId, <code>token3</code> = expert surname</td></tr>
<tr><td><code>yara-resend-documents</code></td><td>Expert requests document resend</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = link</td></tr>
<tr><td><code>yara-signature</code></td><td>Party notified to sign the expert's damage assessment</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = expert surname, <code>token10</code> = link</td></tr>
<tr><td><code>yara-fanavaran-claim</code></td><td>Fanavaran submission confirmed — sent to claim owner with Fanavaran claim number and ID</td><td><code>token</code> = publicId, <code>token2</code> = Fanavaran claimId, <code>token3</code> = Fanavaran claimNo</td></tr>
</table>
<p class="note" style="margin-top:8px;">
All SMS calls are fire-and-forget — they never throw. Failures are logged but do not block the main flow.
An <code>sms_send_logs</code> MongoDB collection records every outbound message with its kind (OTP vs TEMPLATE), provider, template name, and success/failure status.
Notification text messages (parties-disagree, one-party-signed, etc.) are seeded into the <code>sms_texts</code> collection on startup and editable at runtime.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="ai">7 — AI Service <span class="status-badge status-disabled">disabled (code present)</span></h2>
<p class="section-intro">
An image-based car damage detection service is integrated in the
codebase but its HTTP calls are <strong>fully commented out</strong>.
The module initialises on startup, attempts a login (silently swallowed
if it fails), and exposes an <code>aiRequestImage</code> method — but
the underlying axios calls are disabled. The service does not affect
any production flow.
</p>
<div class="card card-yellow">
<h3>Intended interface (when re-enabled)</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/auth/login</code></td><td>Authenticate with username + password. Returns <code>accessToken</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{AI_URL_V2}/auth/profile</code></td><td>Fetch <code>apiKey.key</code> needed as the <code>gateway-api-key</code> request header.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/services/car-damage/detector?version=ai-v7</code></td><td>Submit a car part image (multipart). Returns <code>downloadLink</code> with annotated result.</td></tr>
</table>
<p class="warn" style="margin-top:8px;">
Status: all three calls are wrapped in commented-out <code>axios.request(…)</code> blocks.
<code>CW_URL</code> is not in <code>.env.example</code>. To re-enable, uncomment the login, getApiKey, and aiRequestImage axios calls, and configure <code>AI_URL_V2</code>, <code>AI_USERNAME</code>, <code>AI_PASSWORD</code>.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="car-pricing">8 — Car Pricing Service <span class="status-badge status-partial">partially active</span></h2>
<p class="section-intro">
Used only during damage-expert price-drop calculation. When an expert
provides per-part severity values the system fetches real-time market
prices for the damaged car model, then computes the price-drop using
the formula: <strong>carPrice × yearCoefficient × sumOfPartCoefficients ÷ 400</strong>.
The service has two data sources (endpoints) that are tried in parallel.
</p>
<div class="card card-orange">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?akharin</code></td><td>Fetch car market prices from the "Akharin" source. Returns array of <code>{ carName, marketPrice }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?hamrah</code></td><td>Fetch car market prices from the "Hamrah" source. Same response shape.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
Both endpoints are tried; results are merged and de-duplicated. The best match for
the damaged car's name is found using <strong>Levenshtein distance</strong> (fuzzy string match).
If both endpoints fail or return empty, the price-drop calculation is skipped (marked incomplete) — it does not block claim submission.
</p>
<p class="warn" style="margin-top:6px;">
<strong><code>CW_URL</code> is not documented in <code>.env.example</code>.</strong>
This service will silently produce no price-drop if the variable is unset.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="offline-inquiry">9 — Offline Inquiry <span class="status-badge status-internal">internal / fallback</span></h2>
<p class="section-intro">
The offline inquiry layer intercepts plate-based inquiry calls before
any external HTTP is made. It is primarily used for development and
testing (pre-seeded known plates) but also acts as a resilience fallback
when live inquiry services are unavailable. It is controlled by a
runtime database flag, not an env var.
</p>
<div class="card card-gray">
<h3>How it works</h3>
<table>
<tr><th>Aspect</th><th>Detail</th></tr>
<tr><td>Storage</td><td>MongoDB collection <code>offline-inquiries</code>. Documents contain <code>clientKey</code>, normalised plate fields, <code>nationalCode</code>, and the pre-built <code>raw</code> + <code>mapped</code> response to return.</td></tr>
<tr><td>Master switch</td><td><code>system_settings.offlineInquiry.enabled</code> — defaults to <code>true</code>. Toggle via <code>PATCH /super-admin/system-settings/offline-inquiry</code>.</td></tr>
<tr><td>Lookup order</td><td>Normalised plate (digits-only, Arabic→Persian) + national code + Fanavaran client key must all match. If found, returned immediately; no HTTP call is made.</td></tr>
<tr><td>Scope</td><td>Only applies to plate-based block-inquiry (THIRD_PARTY). CAR_BODY inquiry (<code>/badane</code>) always hits the live API.</td></tr>
<tr><td>Live API flag</td><td><code>system_settings.externalApis.sandHubUseLiveApi</code> — when <code>false</code> (default), even if no offline seed matches, a built-in mock response is returned rather than calling Tejarat/ESG.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="env-ref">10 — Environment Variable Reference</h2>
<p class="section-intro">
All env vars across all integrations, grouped by service.
Variables marked <strong>*</strong> are not present in <code>.env.example</code>.
</p>
<div class="card card-blue">
<h3>Fanavaran</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>FANAVARAN_CLIENT</code></td><td>Active tenant profile key: <code>parsian</code> | <code>tejaratno</code> | <code>moallem</code></td></tr>
<tr><td><code>INSURANCE_CORP_ID</code></td><td>Display caption of the insurer company (e.g. <em>"بیمه پارسیان"</em>) — resolved to a numeric corpId at startup against the Fanavaran insurance-corp list.</td></tr>
</table>
<p class="note" style="margin-top:8px;">Per-tenant credentials (appName, secret, username, password, CorpId, ContractId, Location) are hardcoded in <code>src/core/config/fanavaran-client.config.ts</code> under <code>SEED_FANAVARAN_CLIENT_PROFILES</code>.</p>
</div>
<div class="card card-gray">
<h3>SandHub (legacy)</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>SANHUB_BASE_URL</code></td><td>Base URL for SandHub. Default: <code>http://82.99.202.245:3027</code></td></tr>
<tr><td><code>SANHUB_URL_LOGIN</code></td><td>Full login URL (usually base + <code>/user/login</code>)</td></tr>
<tr><td><code>SANHUB_USERNAME</code></td><td>SandHub login email</td></tr>
<tr><td><code>SANHUB_PASSWORD</code></td><td>SandHub login password</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>Tejarat inquiry</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>TEJARAT_INQUIRY_BASE_URL</code></td><td>Base URL. Default: <code>http://82.99.202.245:3027</code></td></tr>
<tr><td><code>TEJARAT_INQUIRY_EMAIL</code></td><td>Login email</td></tr>
<tr><td><code>TEJARAT_INQUIRY_PASSWORD</code></td><td>Login password</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>ESG (CLIENT_ID=8 only)</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>CLIENT_ID</code></td><td>Set to <code>8</code> to activate the ESG inquiry provider for the Parsian tenant.</td></tr>
<tr><td><code>ESG_URL</code></td><td>ESG base URL. Default: <code>http://192.168.20.22:8085</code> (internal network)</td></tr>
<tr><td><code>ESG_USERNAME</code></td><td>ESG login username</td></tr>
<tr><td><code>ESG_PASSWORD</code></td><td>ESG login password</td></tr>
</table>
</div>
<div class="card card-green">
<h3>SMS</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>kavenegar</code> (default) or <code>parsian</code></td></tr>
<tr><td><code>SMS_API_KEY</code></td><td>Kavenegar API key (required when provider = kavenegar)</td></tr>
<tr><td><code>AUTH_SMS_TEMPLATE</code></td><td>Kavenegar template name for OTP messages (e.g. <code>yara-otp</code>)</td></tr>
<tr><td><code>PARSIAN_SMS_URL</code></td><td>Parsian SMS Gateway base URL (required when provider = parsian)</td></tr>
<tr><td><code>PARSIAN_API_KEY</code></td><td>Parsian SMS <code>X-PACKAGE-API-KEY</code> header value</td></tr>
<tr><td><code>PARSIAN_BASIC_TOKEN</code></td><td>Base64-encoded credentials for <code>Authorization: Basic …</code> header</td></tr>
<tr><td><code>URL</code></td><td>Frontend base URL — used to build all invite + claim links embedded in SMS messages</td></tr>
</table>
</div>
<div class="card card-yellow">
<h3>AI service</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>AI_URL_V2</code></td><td>AI gateway base URL. Default: <code>https://ai-gw.ittalie.ir</code> (unused — service is disabled)</td></tr>
<tr><td><code>AI_USERNAME</code></td><td>AI service login username (unused)</td></tr>
<tr><td><code>AI_PASSWORD</code></td><td>AI service login password (unused)</td></tr>
</table>
</div>
<div class="card card-orange">
<h3>Car pricing service</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>CW_URL</code> *</td><td>Base URL for car market price API (e.g. <code>https://…/</code>). Not in <code>.env.example</code>. Price-drop silently skipped if unset.</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>General / app</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>PORT</code></td><td>HTTP port (default 3000). Used by the Fanavaran insurance-corp fallback to call its own local lookup endpoint.</td></tr>
<tr><td><code>CAPTCHA_ENABLED</code></td><td><code>true</code> / <code>false</code> — enables/disables login CAPTCHA challenge. Internal, no external service.</td></tr>
<tr><td><code>EXP_CAPTCHA_TIME</code></td><td>CAPTCHA challenge TTL in minutes.</td></tr>
<tr><td><code>EXP_OTP_TIME</code></td><td>OTP TTL in minutes.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,345 @@
---
last_updated: 2026-08-08
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12]
source: fanavaran-module-docs
---
# 01 — Write APIs (Claim Registration)
Base host:
```text
https://apimanager.iraneit.com/BimeApiManager/api
BimeApi v2: .../api/BimeApi/v2.0
```
Common business headers (after Login):
| Header | Source |
|--------|--------|
| `authenticationToken` | Login response |
| `CorpId` | Tenant auth |
| `ContractId` | Tenant auth |
| `Location` | Tenant auth |
| `Content-Type` | `application/json` (except GEN.07 multipart) |
Template values below are **Parsian-proven** unless noted.
---
## 1. GetAppToken
| Item | Value |
|------|-------|
| نام عملیات | دریافت App Token |
| Endpoint | `POST /api/EITAuthentication/GetAppToken` |
| Method | `POST` |
| Body | Empty (`Content-Length: 0`; no JSON) |
| Authentication | Headers `appname`, `secret` (tenant) |
**Headers**
| Header | Required | Notes |
|--------|----------|-------|
| `appname` | ✅ | Tenant `auth.appName` |
| `secret` | ✅ | Tenant `auth.secret` |
**Success:** token in response header `appToken` / `apptoken`.
**Error:** invalid app credentials → Fanavaran error body/message.
**YARA:** `FanavaranAuthService` → `getAppTokenUrl`.
---
## 2. Login
| Item | Value |
|------|-------|
| نام عملیات | ورود و دریافت authenticationToken |
| Endpoint | `POST /api/EITAuthentication/Login` |
| Method | `POST` |
| Body | Empty |
| Authentication | `appToken` + `userName` + `password` |
**Headers**
| Header | Required |
|--------|----------|
| `appToken` | ✅ fresh from GetAppToken |
| `userName` | ✅ |
| `password` | ✅ |
**Success:** `authenticationToken` in header or body.
**Cache (YARA):** until next **Asia/Tehran midnight** — memory + Mongo `fanavaranAuthTokens`.
**Error example:** `نام کاربر یا رمز عبور صحیح نیست` (wrong password or stale/wrong appToken).
**YARA:** `FanavaranAuthService.getAuthenticationToken(clientKey)`.
---
## 3. GEN.03 — Base claim create
Doc: `CAR.THID.APIH.GEN.03`
| Item | Value |
|------|-------|
| نام عملیات | ایجاد پرونده خسارت مالی ثالث |
| Endpoint | `POST /Api/BimeApi/v2.0/car/third-party-car-financial-claims` |
| Method | `POST` |
| Auth | Business headers |
Also available: `GET .../{claimid}`, `GET ...?{ODATA}`.
### Key request fields
| Field | Parsian template | Notes |
|-------|------------------|-------|
| `PolicyId` | from inquiry | **Required** for submit |
| `ClaimExpertId` | `154` | GEN.03 role = مسئول پرونده مالی (≠ GEN.08) |
| `AccidentCityId` | `701` | Shared default |
| `AccidentReportTypeId` | `155` | Shared |
| `AccidentVehicleUsedId` | `1` | Shared |
| `CompensationReferenceId` | `167` | Shared |
| `CulpritLicenceTypeId` | `2` | Shared |
| `CulpritTypeId` | `337` | Shared |
| `AccidentCauseId` | `6` | Shared code default |
| `AccidentDate` / `AnnouncementDate` / `DocReceivedDate` | Jalali from blame/claim time | |
| `AccidentTime` | `HH:mm` | |
| `AccidentLocationAddress` | `استان تهران شهر تهران` | Provisional constant |
| `EstimateAmount` | ≥ 1; provisional `1000` | Must be positive |
| `CulpritLicenceNo` | real or dummy | Never empty |
| `CulpritLicenceIssuDate` | party / default | |
| Many others | `null` | **Keep explicit nulls** — do not omit |
Full shape: see skill reference sample and `buildFanavaranSubmitPayload` / `applyFanavaranDefaultFields`.
### Validation (YARA)
- Only `THIRD_PARTY` claims (not `CAR_BODY`)
- `PolicyId` required on submit (`requirePolicyId: true`)
- Skip if local `claimId` / `claimNo` already set
- `EstimateAmount` normalized to positive
### Success response
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.claimId` |
| `ClaimNo` | `claimCases.claimNo` |
History: `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED`.
### Error handling
- Never throw out of normal user claim flow on auto-submit
- Persist `fanavaranSync.baseClaim.status=failed`, `lastError`, schedule retry
- Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/base-claim/submit`
### Sample (Parsian-shaped)
```json
{
"AccidentCityId": 701,
"AccidentReportTypeId": 155,
"AccidentVehicleUsedId": 1,
"ClaimExpertId": 154,
"CompensationReferenceId": 167,
"CulpritLicenceTypeId": 2,
"CulpritTypeId": 337,
"AccidentCauseId": 6,
"AccidentDate": "1405/04/05",
"AnnouncementDate": "1405/04/05",
"DocReceivedDate": "1405/04/05",
"AccidentTime": "08:03",
"AccidentLocationAddress": "استان تهران شهر تهران",
"EstimateAmount": 1000,
"PolicyId": 13764408,
"CulpritLicenceNo": "1124242",
"CulpritLicenceIssuDate": "1394/10/13",
"DamagedCount": 1,
"IsLicenseMatchWithVehicleKind": 1,
"HasOtherCulprit": 0,
"IsAccidentOutOfBorder": 0,
"IsFatalAccident": 0,
"IsPlaqueChanged": 0,
"PoliceOfficerId": 1,
"IsLicenseReplacement": 0,
"PreviousPolicyEndDate": "",
"ActualPremium": null,
"ArchiveNo": null
}
```
Proven Parsian: `claimId=4909952`, `claimNo=1632`, `policyId=13764408`.
---
## 4. GEN.12 — Damage case
Doc: `CAR.THID.APIH.GEN.12`
| Item | Value |
|------|-------|
| نام عملیات | ثبت مورد خسارت (خودرو/شخص زیان‌دیده) |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/dmg-cases` |
| Method | `POST` |
Requires existing `claimId` (soft-ensures GEN.03 if missing).
### Key request fields
| Field | Source |
|-------|--------|
| `Desc` | Joined selected outer part labels (`سپر عقب/...`) |
| `DriverId` | Fanavaran person inquiry (cached) |
| `VehicleKindId` | Lookup match on `claimCase.vehicle.carType` |
| `InsuranceCorpId` | Resolve from `INSURANCE_CORP_ID` caption |
| `ChassisNo` / `MotorNo` / `VIN` / plate fields | Party vehicle inquiry |
| `PolicyNo` / `PolicyCINumber` / dates | Inquiry aliases |
| `LicenceNo` | Driver/insurer licence; never empty |
| `EstimateAmount` | Provisional `1000` early |
| `DmgCaseTypeId` / `DmgHistoryStatus` / plate kinds | Tenant defaults |
### Success
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.dmgCaseId` |
History: `FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED`.
Proven Parsian: `dmgCaseId=427594`, `DriverId=2426953`.
### Trigger
After local `SELECT_OUTER_PARTS` — before image upload.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/damage-case/submit`.
---
## 5. GEN.07 — Attachments
Doc: `CAR.THID.APIH.GEN.07`
| Item | Value |
|------|-------|
| نام عملیات | آپلود فایل پیوست پرونده |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/files` |
| Method | `POST` |
| Content-Type | `multipart/form-data` (boundary by FormData) |
### Multipart shape
| Part name | Content |
|-----------|---------|
| `content` | JSON string (`application/json`) with `FileName`, `FileTypeId`, optional `Files[]` |
| `files` | Real file bytes (one request per local image) |
**Do not** base64-encode. Filenames in JSON must match multipart filenames.
### FileTypeId (critical — tenant-specific)
| Tenant | `ClaimFileTypeId` | Note |
|--------|-------------------|------|
| **parsian** | **63** | `ساير مدارک خسارت` — proven. **Do not use 70** |
| tejaratno | `23` | Default |
| moallem | shared `23` until confirmed | Verify in `file-types` lookup |
Fanavaran error if id missing from tenant lookup:
`مقدار فیلد نوع فايل با منبع لوکاپ مطابقت ندارد`.
### Success
File ids recorded under `fanavaranSync.attachments.files[]`.
Best-effort: failures audited + retried; local user flow continues.
No videos unless Fanavaran confirms support.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/attachments/submit`.
---
## 6. GEN.08 — Expertise
Doc: `CAR.THID.APIH.GEN.08`
| Item | Value |
|------|-------|
| نام عملیات | ثبت کارشناسی خسارت مالی ثالث |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/expertise` |
| Method | `POST` |
Requires `claimId` + `dmgCaseId` (soft-ensures earlier stages).
### Key request fields
| Field | Mapping |
|-------|---------|
| `ClaimExpertId` | Tenant **`ExpertiseClaimExpertId`** (Parsian `29`) — assessor role |
| `DmgCaseId` | Local `dmgCaseId` |
| `RepairWage` | Sum of part `salary` |
| `ComponentReplacementCost` | Sum of part `price` |
| `WasteValue` | Sum of `daghi.price` |
| `DmgAssessmentDate` / `InspectionTime` | From expert reply submit time |
| `InspectionPlaceId` | Currently `282` (code constant; verify per tenant lookup) |
| `DropAmountStatus` | Currently `5458` |
| `DropAmountAdditionsDeductions` | `evaluation.priceDrop.total` |
| `DamagedVehicleCurrentPrice` | `evaluation.priceDrop.carPrice` |
| `DmgSections[]` | One row per priced part |
`DmgSections[]` row:
| Field | Source |
|-------|--------|
| `DmgSectionId` | Fanavaran car-components / part id |
| `AccidentLevel` | Part damage type / default `5456` |
| `Desc` | Damage type / label |
| `RepairWage` / `ComponentReplacementCost` / `WasteValue` | Line amounts |
### Validation
- Active expert reply with submit-ready parts
- Factor-needed lines wait until `totalPayment > 0`
- Missing `DmgCaseId` / section ids → `BadRequestException` with `warnings[]`
### Success
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.expertiseId` |
History: `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED`.
SMS owner via `SmsOrchestrationService` (deduped `fanavaranSync.expertise.smsNotifiedAt`).
Proven Parsian: `expertiseId=403144`, `ClaimExpertId=29`.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/expertise/submit`.
---
## Follow-up APIs (documented upstream, not all wired)
| Doc | Purpose |
|-----|---------|
| GEN.09 | Damaged points |
| GEN.10 | `dmg-department-referral` |
| GEN.11 | Cancel expertise |
| GEN.13 | Drop amounts GET |
| GEN.14 | Culprit damaged points |
Current YARA production path focuses on GEN.03 → 12 → 07 → 08.
---
## YARA manual HTTP surface
Controller: `FanavaranController` (`src/fanavaran/fanavaran.controller.ts`)
Prefix: `/v2/fanavaran` — Bearer + `LocalActorAuthGuard`.
| Method | Path |
|--------|------|
| GET | `/clients` |
| GET/POST | `/:client/claim-cases/:id/base-claim/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/damage-case/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/attachments/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/expertise/preview\|submit` |
`:client` ∈ `parsian` \| `tejaratno` \| `moallem`.

View File

@@ -0,0 +1,191 @@
---
last_updated: 2026-08-09
tags: [fanavaran, api, read, lookups]
source: fanavaran-module-docs
---
# 02 — Read APIs & Lookups
All read calls use the same business headers as write APIs (`authenticationToken`, `CorpId`, `ContractId`, `Location`).
Lookup responses are **tenant-specific**. Cache directory:
```text
files/fanavaran-lookups/{clientKey}/
```
Config: `src/fanavaran/fanavaran-lookup.config.ts`
Service: `FanavaranLookupService`
Local HTTP: `LookupsController` / `LookupsService` (`src/lookups/`)
Fanavaran base (docs often call this **BaseURL1**):
```text
https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0
```
Example catalogue URL:
```text
{BaseURL1}/car/base-info/driving-licence-types
→ YARA: GET /lookups/driving-licence-types
```
---
## 1. Policy inquiry (guilty party)
| Item | Value |
|------|-------|
| Endpoint | `GET /Api/BimeApi/v2.0/common/Policies/inquiry-my-policies` |
| Query | `InsuranceLineId=5` (third-party), `NationalCode={insurerNationalCode}` |
| Usage | Resolve `PolicyId` for GEN.03 |
| Selection | `selectLatestActiveFanavaranPolicy` — latest non-expired by `EndDate` |
| Cache | `fanavaranSync.baseClaim.policyId` (resolve-once) |
| Force refresh | Manual preview `forceRefreshPolicy=true` only |
**Errors (YARA messages):**
- No policies → contact admin
- Latest expired → cannot send
- Invalid PolicyId/EndDate → contact admin
**Do not** treat UI `resolvePolicy=true` as cache-bust (deprecated; ignored for re-inquiry).
---
## 2. Driver / person inquiry
Used when building GEN.12 to resolve `DriverId`.
| Item | Value |
|------|-------|
| Endpoint | `GET /Api/BimeApi/v2.0/common/parties/inquiry-by-unique-identifier` |
| Usage | `resolveDriverFanavaranId(clientKey, nationalCode, birthday, driverIsInsurer)` via `FanavaranLookupService` |
| Cache | `fanavaranSync.damageCase.driverId` and `blameCase.parties[].person.fanavaranDriverId` |
Prefer cache before live call.
---
## 3. Remote lookup catalogue
Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
| Name | Fanavaran path | Local YARA route | Used for |
|------|----------------|------------------|----------|
| accident-causes | `/car/base-info/accident-causes` | `GET /lookups/accident-causes` | Accident cause options |
| accident-report-type | `/car/code-list/accident-report-type` | `GET /lookups/accident-report-type` | Defaults / UI |
| vehicle-use-types | `/car/base-info/vehicle-use-types` | `GET /lookups/vehicle-use-types` | Vehicle use |
| dmg-pay-method | `/car/code-list/dmg-pay-method` | `GET /lookups/dmg-pay-method` | Pay method |
| driving-licence-types | `/car/base-info/driving-licence-types` | `GET /lookups/driving-licence-types` | Licence type |
| accident-culprit-type | `/car/code-list/accident-culprit-type` | `GET /lookups/accident-culprit-type` | Culprit type |
| inspection-place | `/car/code-list/inspection-place` | `GET /lookups/inspection-place` | GEN.08 |
| drop-amount-status | `/car/code-list/drop-amount-status` | `GET /lookups/drop-amount-status` | GEN.08 |
| car-components | `/car/base-info/car-components` | `GET /lookups/car-components` | `DmgSectionId` |
| accident-level | `/car/code-list/accident-level` | `GET /lookups/accident-level` | Section severity |
| expert-status | `/car/code-list/expert-status` | `GET /lookups/expert-status` | Expertise status |
| vehicle-kinds | `/car/base-info/vehicle-kinds` | `GET /lookups/vehicle-kinds` | GEN.12 `VehicleKindId` |
| person-role | `/common/code-list/person-role` | `GET /lookups/person-role` | Roles |
| insurance-corp | `/common/code-list/insurance-corp` | via fanavaran + resolve | GEN.12 `InsuranceCorpId` |
| file-types | `/common/base-info/file-types` | `GET /lookups/file-types` | GEN.07 `FileTypeId` |
| cities | `/common/base-info/cities` | `GET /lookups/cities` | City ids |
| provinces | `/common/base-info/Provinces` | `GET /lookups/provinces` | Provinces |
| dmg-case-type | `/car/code-list/dmg-case-type` | `GET /lookups/dmg-case-type` | GEN.12 |
| dmg-history-status | `/car/code-list/dmg-case-history-status` | `GET /lookups/dmg-history-status` | GEN.12 |
| used-place | `/car/code-list/used-place` | `GET /lookups/used-place` | Used place |
| dmg-business-line | `/car/code-list/dmg-business-line` | `GET /lookups/dmg-business-line` | Business line |
### Generic accessor
```http
GET /lookups/fanavaran
GET /lookups/fanavaran/{lookupName}
```
Lists / fetches by name for the **active** `FANAVARAN_CLIENT`.
---
## 4. Lookups module — Nest routes (`LookupsController`)
Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
### 4.1 Cached Fanavaran catalogue (dedicated routes)
| Nest route | Fanavaran path |
|------------|----------------|
| `GET /lookups/accident-causes` | `/car/base-info/accident-causes` |
| `GET /lookups/accident-report-type` | `/car/code-list/accident-report-type` |
| `GET /lookups/vehicle-use-types` | `/car/base-info/vehicle-use-types` |
| `GET /lookups/dmg-pay-method` | `/car/code-list/dmg-pay-method` |
| `GET /lookups/driving-licence-types` | `/car/base-info/driving-licence-types` |
| `GET /lookups/accident-culprit-type` | `/car/code-list/accident-culprit-type` |
| `GET /lookups/inspection-place` | `/car/code-list/inspection-place` |
| `GET /lookups/drop-amount-status` | `/car/code-list/drop-amount-status` |
| `GET /lookups/car-components` | `/car/base-info/car-components` |
| `GET /lookups/accident-level` | `/car/code-list/accident-level` |
| `GET /lookups/expert-status` | `/car/code-list/expert-status` |
| `GET /lookups/vehicle-kinds` | `/car/base-info/vehicle-kinds` |
| `GET /lookups/person-role` | `/common/code-list/person-role` |
| `GET /lookups/file-types` | `/common/base-info/file-types` |
| `GET /lookups/cities` | `/common/base-info/cities` |
| `GET /lookups/provinces` | `/common/base-info/Provinces` |
| `GET /lookups/dmg-case-type` | `/car/code-list/dmg-case-type` |
| `GET /lookups/dmg-history-status` | `/car/code-list/dmg-case-history-status` |
| `GET /lookups/used-place` | `/car/code-list/used-place` |
| `GET /lookups/dmg-business-line` | `/car/code-list/dmg-business-line` |
| `GET /lookups/fanavaran` | catalogue metadata (names + URLs) |
| `GET /lookups/fanavaran/{lookupName}` | any configured name (incl. `insurance-corp`) |
### 4.2 Live inquiry helpers
| Nest route | Fanavaran path / notes |
|------------|------------------------|
| `GET /lookups/inquiry-by-vin?vin=` | `/car/vehicles/inquiry-by-vin` |
| `GET /lookups/my-policies?nationalCode=&insuranceLineId=` | `/common/Policies/inquiry-my-policies` |
| `GET /lookups/third-party-policy/:policyId` | `/car/third-party-car-policies/{id}` |
| `GET /lookups/body-policy/:policyId` | `/car/vehicle-hull-policies/{id}` |
### 4.3 UI accident field helpers (local / mapped)
| Nest route | Purpose |
|------------|---------|
| `GET /lookups/accident-way` | Accident way options |
| `GET /lookups/accident-reason` | Accident reason options (+ Fanavaran map when available) |
| `GET /lookups/accident-type` | Accident type options |
| `GET /lookups/accident-fields` | Combined way + reason + type |
---
## 5. Insurance corp resolve
| Item | Value |
|------|-------|
| Env | `INSURANCE_CORP_ID` = Persian **Caption** in insurance-corp list |
| Method | `FanavaranLookupService.resolveInsuranceCorpId(clientKey)` |
| Behavior | Fetch list once, match caption → numeric `Id`, cache |
| Parsian proven | `InsuranceCorpId=329` (for that deployment caption) |
---
## 6. Vehicle kind resolve
| Item | Value |
|------|-------|
| Input | Local `claimCase.vehicle.carType` (sedan/suv/…) |
| Method | Match Fanavaran `vehicle-kinds` Caption keywords |
| Cache | `fanavaranSync.damageCase.vehicleKindId` |
| Parsian proven | `VehicleKindId=6704` on sample case |
---
## 7. How lookups are used in project
1. First call for a tenant fetches from Fanavaran (authenticated).
2. Writes JSON under `files/fanavaran-lookups/{client}/`.
3. Subsequent reads prefer cache file.
4. Claim payload builders call `getFanavaranLookupRows(clientKey, name)` when mapping ids.
5. Outer car parts catalogue can align with Fanavaran car-components (`FANAVARAN_CAR_PARTS_CATALOG` / helpers).
**Dependency:** Auth must succeed for cold cache. Transient “try again later” triggers tenant backoff (see [09-error-handling.md](./09-error-handling.md)).

View File

@@ -0,0 +1,111 @@
---
last_updated: 2026-08-08
tags: [fanavaran, constants, config]
source: fanavaran-module-docs
---
# 03 — Constants
Legend for **Varies by company?**
✅ = different per insurer tenant · ❌ = shared across tenants in current code
Secrets are **not** printed here — read from Mongo `fanavaranClientConfigs` or seed profiles.
---
## Auth (tenant)
| Name | Example (Parsian) | Where used | Why | Varies? |
|------|-------------------|------------|-----|---------|
| `appName` | `ParsianService` | GetAppToken | App identity | ✅ |
| `secret` | *(config)* | GetAppToken | App secret | ✅ |
| `username` | `ParsianServiceUser` | Login | User | ✅ |
| `password` | *(config)* | Login | Password | ✅ |
| `corpId` | `543` | All business calls | Corp scope | ✅ |
| `contractId` | `28` | All business calls | Contract | ✅ |
| `location` | `210050` | All business calls / OpBUId | Branch/location | ✅ |
Seed: `SEED_FANAVARAN_CLIENT_PROFILES` · Runtime: `getFanavaranClientProfile(key)`.
---
## Payload defaults (tenant profile.defaults)
| Name | Shared seed | Parsian override | Tejaratno | Moallem | Varies? |
|------|-------------|------------------|-----------|---------|---------|
| `AccidentCityId` | 701 | same | same | same | usually ❌ |
| `AccidentReportTypeId` | 155 | same | same | same | usually ❌ |
| `AccidentVehicleUsedId` | 1 | same | same | same | usually ❌ |
| `ClaimExpertId` (GEN.03) | 4543092 | **154** | 4543092 | shared TBD | ✅ |
| `ExpertiseClaimExpertId` (GEN.08) | 2709 | **29** | **2709** | shared TBD | ✅ |
| `CompensationReferenceId` | 167 | same | same | same | usually ❌ |
| `CulpritLicenceTypeId` | 2 | same | same | same | usually ❌ |
| `CulpritTypeId` | 337 | same | same | same | usually ❌ |
| `DmgCaseTypeId` | 175 | same | same | same | may ✅ |
| `DmgHistoryStatus` | 5214 | same | same | same | may ✅ |
| `PlaqueKindId` | 8 | same | same | same | may ✅ |
| `PlaqueSampleId` | 10 | same | same | same | may ✅ |
| `DriverIsOwner` | 0 | same | same | same | ❌ |
| `FaultPercent` | 100 | same | same | same | ❌ |
| `ClaimFileTypeId` (GEN.07) | 23 | **63** | 23 | 23 TBD | ✅ |
**Never swap** GEN.03 `ClaimExpertId` with GEN.08 `ExpertiseClaimExpertId` — different Fanavaran roles.
---
## Code-level shared constants
File: `claim-request-management.service.ts`
| Name | Value | Where | Why | Varies? |
|------|-------|-------|-----|---------|
| `FANAVARAN_SUBMIT_URL` | `.../third-party-car-financial-claims` | All stage posts | API root | ❌ (same host) |
| `FANAVARAN_ACCIDENT_LOCATION_ADDRESS` | `استان تهران شهر تهران` | GEN.03 | Provisional address | currently ❌ |
| `FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID` | `6` | GEN.03 | Default cause | may ✅ later |
| `FANAVARAN_DEFAULT_ACCIDENT_LEVEL` | `5456` | GEN.08 sections | Default severity | may ✅ |
| `FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT` | `1000` | GEN.03/12 early | Positive estimate | ❌ |
| `FANAVARAN_DUMMY_LICENCE_NO` | env or `9705463515` | Licence fields | Never empty | env override |
| `InspectionPlaceId` (expertise) | `282` | GEN.08 | Hardcoded today | should verify ✅ |
| `DropAmountStatus` (expertise) | `5458` | GEN.08 | Hardcoded today | should verify ✅ |
| Auth URLs | apimanager.iraneit.com | Auth service | Host | ❌ |
| Lookup base | same host `/BimeApi/v2.0` | Lookups | Host | ❌ |
---
## Env / activation
| Name | Example | Where | Why | Varies? |
|------|---------|-------|-----|---------|
| `FANAVARAN_CLIENT` | `parsian` | `resolveFanavaranClientKey` | Active tenant | ✅ per deploy |
| `CLIENT_ID` | `8` → parsian | Fallback hint | Legacy deploy map | ✅ |
| `INSURANCE_CORP_ID` | Persian caption | Damage `InsuranceCorpId` | Match lookup Caption | ✅ |
| `FANAVARAN_DUMMY_LICENCE_NO` | digits | Licence fallback | Test/prod dummy | optional |
---
## Enums / claim types (YARA)
| Name | Relevant values | Fanavaran use |
|------|-----------------|---------------|
| Claim type | `THIRD_PARTY` only | Fanavaran path enabled |
| | `CAR_BODY` | **Not** submitted to Fanavaran |
| `ClaimCaseStatus` | local workflow | Triggers stages indirectly |
| History event types | `FANAVARAN_*_SUCCEEDED/FAILED` | Audit trail on claim |
---
## Retry / backoff constants
| Name | Value | Where |
|------|-------|-------|
| `FANAVARAN_RETRY_DELAY_MS` | 5 min | Stage retry |
| `FANAVARAN_TRANSIENT_RETRY_DELAY_MS` | 10 min | “Try again later” stage retry |
| `FANAVARAN_MAX_RETRIES` | 2 | Per stage |
| `TRANSIENT_BACKOFF_MS` | 5 min | Tenant-wide auth backoff |
| Token TTL | Until Tehran midnight | Auth cache |
---
## Plate letter codes
`FANAVARAN_PLATE_LETTER_CODE` maps Persian plate letters → Fanavaran middle codes (shared mapping table in claim service).

View File

@@ -0,0 +1,63 @@
---
last_updated: 2026-08-08
tags: [fanavaran, tenant, matrix]
source: fanavaran-module-docs
---
# 04 — Tenant-specific vs shared
## Comparison table
| مورد | مشترک | وابسته به شرکت |
|------|:-----:|:--------------:|
| API host (`apimanager.iraneit.com`) | ✅ | ❌ |
| Endpoint paths (GEN.03/07/08/12, auth) | ✅ | ❌ |
| HTTP methods & payload **shape** | ✅ | ❌ |
| Auth flow (GetAppToken → Login) | ✅ | ❌ |
| Auth credentials (app/user/secret) | ❌ | ✅ |
| `CorpId` / `ContractId` / `Location` | ❌ | ✅ |
| `ClaimExpertId` (GEN.03) | ❌ | ✅ |
| `ExpertiseClaimExpertId` (GEN.08) | ❌ | ✅ |
| `ClaimFileTypeId` (GEN.07) | ❌ | ✅ |
| Lookup **Id values** (file-types, experts, …) | ❌ | ✅ |
| Lookup **endpoint URLs** | ✅ | ❌ |
| Policy inquiry algorithm | ✅ | ❌ |
| YARA orchestration / soft-ensure / retry | ✅ | ❌ |
| YARA internal routes `/v2/fanavaran/...` | ✅ | path `:client` only |
| `INSURANCE_CORP_ID` caption / resolved id | ❌ | ✅ |
| `FANAVARAN_CLIENT` env | ❌ | ✅ per deploy |
| Mapping YARA→Fanavaran field names | ✅ | ❌ |
| Validation: PolicyId required, EstimateAmount > 0 | ✅ | ❌ |
| Validation: FileTypeId ∈ tenant lookup | logic ✅ | allowed ids ✅ |
| Business: THIRD_PARTY only | ✅ | ❌ |
| Provisional address / dummy licence | ✅ today | may customize later |
| SMS template after expertise | shared orchestration | provider/templates may ✅ |
| Mongo collections / audit schema | ✅ | ❌ |
| Offline inquiry seeds | may exist per client | ✅ |
## Current tenants
| Key | Status | Notes |
|-----|--------|-------|
| `parsian` | **Template / proven E2E** | Use as reference for new clients |
| `tejaratno` | Working shape | Different experts + FileTypeId 23 |
| `moallem` | Auth seeded | Confirm expert + file-type ids via lookups before go-live |
## What is reusable when adding a client
1. Entire staged pipeline in `ClaimRequestManagementService`
2. `FanavaranAuthService`, audit, lookup cache machinery
3. Controllers + preview/submit surface
4. Policy selection helper
5. Soft-ensure + retry + history events
## What must be configured per client
1. Auth block + Corp/Contract/Location
2. GEN.03 / GEN.08 expert ids (from that tenant’s expert lists)
3. `ClaimFileTypeId` present in that tenant’s `file-types.json`
4. `INSURANCE_CORP_ID` caption for deploy
5. Warm lookups under `files/fanavaran-lookups/{key}/`
6. Optional: override city/cause/inspection ids if shared defaults fail validation
See [11-onboard-new-client.md](./11-onboard-new-client.md).

View File

@@ -0,0 +1,130 @@
---
last_updated: 2026-08-08
tags: [fanavaran, flow, sequence]
source: fanavaran-module-docs
---
# 05 — Full claim registration flow
Applies to **THIRD_PARTY** claims only. Template behavior = **Parsian**.
## High-level stages
```text
Start local claim
→ Validate THIRD_PARTY + data readiness
→ Auth (cached token)
→ Policy inquiry (resolve-once PolicyId)
→ GEN.03 base claim → store claimId / claimNo
→ SELECT_OUTER_PARTS
→ GEN.12 damage case → store dmgCaseId
→ Upload docs/images locally
→ GEN.07 attachments (per file, best-effort)
→ Expert pricing ready
→ GEN.08 expertise → store expertiseId → SMS owner
→ End (local completion independent of Fanavaran success)
```
## Sequence diagram
```mermaid
sequenceDiagram
autonumber
participant User as User / Expert
participant YARA as YARA ClaimRequestManagement
participant Auth as FanavaranAuthService
participant FV as Fanavaran API
participant DB as Mongo claimCases
User->>YARA: Create THIRD_PARTY claim
YARA->>DB: Persist claimCase
YARA->>YARA: autoSubmitToFanavaranV2OnClaimCreated
YARA->>Auth: getAuthenticationToken(client)
alt cache miss / past Tehran midnight
Auth->>FV: POST GetAppToken
Auth->>FV: POST Login
Auth->>DB: fanavaranAuthTokens
end
Auth-->>YARA: authenticationToken
alt no cached policyId
YARA->>FV: GET inquiry-my-policies
YARA->>DB: fanavaranSync.baseClaim.policyId
end
YARA->>FV: POST third-party-car-financial-claims (GEN.03)
FV-->>YARA: Id, ClaimNo
YARA->>DB: claimId, claimNo, history SUCCESS
User->>YARA: SELECT_OUTER_PARTS
YARA->>YARA: autoSubmitFanavaranDamageCase...
Note over YARA: soft-ensure base if missing
YARA->>FV: POST .../dmg-cases (GEN.12)
FV-->>YARA: DmgCaseId
YARA->>DB: dmgCaseId
User->>YARA: Upload images/docs
loop each image not yet uploaded
YARA->>FV: POST .../files multipart (GEN.07)
YARA->>DB: attachments.files[]
end
User->>YARA: Expert reply priced
YARA->>YARA: autoSubmitFanavaranExpertise...
Note over YARA: soft-ensure damage/base
YARA->>FV: POST .../expertise (GEN.08)
FV-->>YARA: ExpertiseId
YARA->>DB: expertiseId, history SUCCESS
YARA->>YARA: SMS owner (deduped)
```
## Flow diagram (stages + soft-ensure)
```mermaid
flowchart TD
A[Local claim created THIRD_PARTY] --> B{claimId exists?}
B -->|No| C[GEN.03 Base claim]
B -->|Yes| D[Skip base]
C --> E[Store claimId/claimNo]
E --> F[Outer parts selected]
D --> F
F --> G{dmgCaseId exists?}
G -->|No| H[Ensure base then GEN.12]
G -->|Yes| I[Skip damage]
H --> J[Store dmgCaseId]
J --> K[Local media upload]
I --> K
K --> L[GEN.07 per file]
L --> M[Expert pricing ready]
M --> N{expertiseId exists?}
N -->|No| O[Ensure damage then GEN.08]
N -->|Yes| P[Done]
O --> P
```
## Local triggers
| Stage | Auto trigger | Function |
|-------|--------------|----------|
| Base | Claim create (early) | `autoSubmitToFanavaranV2OnClaimCreated` |
| Base | Claim completed (legacy/fallback; skips if already submitted) | `autoSubmitToFanavaranV2OnClaimCompleted` |
| Damage | After outer parts selection | `autoSubmitFanavaranDamageCaseOnOuterPartsSelected` |
| Attachments | After successful local image/doc upload | `autoSubmitFanavaranAttachment` |
| Expertise | After expert reply when priced | `autoSubmitFanavaranExpertiseOnExpertReply` |
V5 note: when `requiresFileMakerApproval=true`, Fanavaran submit may wait for FileMaker approval per claim flow rules.
## Manual retry
Use `/v2/fanavaran/{client}/claim-cases/{claimCaseId}/.../submit` for any failed stage. Preview endpoints build payload without requiring submit success.
## Status persistence
Per stage under `claimCases.fanavaranSync.{baseClaim|damageCase|attachments|expertise}`:
- `status`: success / failed / pending / skipped
- `lastError`, `lastTriedAt`, `retryCount`, `nextRetryAt`
- Cached ids + `lastPayload`
Top-level: `claimId`, `claimNo`, `dmgCaseId`, `expertiseId`.

View File

@@ -0,0 +1,140 @@
---
last_updated: 2026-08-08
tags: [fanavaran, yara, integration]
source: fanavaran-module-docs
---
# 06 — YARA ↔ Fanavaran integration points
## Module map
| Concern | Module | Primary files |
|---------|--------|---------------|
| HTTP surface | `FanavaranModule` | `fanavaran.controller.ts` |
| Auth | `FanavaranModule` | `fanavaran-auth.service.ts` |
| Lookups | `FanavaranLookupModule` / `LookupsModule` | `fanavaran-lookup.service.ts`, `lookups.*` |
| Audit | `FanavaranAuditModule` | `fanavaran-audit.service.ts` |
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
| Policy select | same | `fanavaran-policy-selection.ts` |
| SMS after expertise | `SmsOrchestrationModule` | `sms-orchestration.service.ts` |
---
## Interaction catalogue
### A. Authentication
| | |
|--|--|
| Module | `src/fanavaran` |
| Service | `FanavaranAuthService` |
| Controller | *(none external — used by claim/lookup)* |
| Functions | `getAuthenticationToken`, GetAppToken + Login internals |
| When | Before any Fanavaran business/lookup HTTP |
| Send | appname/secret → appToken → username/password |
| Receive | `authenticationToken` |
| Store | Memory cache + `fanavaranAuthTokens` |
| Consumers | Claim submit, lookups, policy inquiry |
### B. Base claim preview/submit
| | |
|--|--|
| Module | Fanavaran + ClaimRequestManagement |
| Controller | `FanavaranController.preview` / `submit` |
| Service | `ClaimRequestManagementService` |
| Functions | `previewFanavaranSubmitV2`, `submitFanavaranV2`, `executeFanavaranV2Submit`, `buildFanavaranSubmitPayload`, `getPolicyIdFromNationalCode` |
| When | Auto on claim create; manual preview/submit |
| Send | GEN.03 JSON + business headers |
| Receive | `Id`, `ClaimNo` |
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
| Consumers | Damage/expertise soft-ensure; UI claim detail; SMS |
### C. Damage case
| | |
|--|--|
| Controller | `previewDamageCase` / `submitDamageCase` |
| Functions | `previewFanavaranDamageCaseV2`, `submitFanavaranDamageCaseV2`, `buildFanavaranDamageCasePayload`, `autoSubmitFanavaranDamageCaseOnOuterPartsSelected`, `ensureFanavaranBaseClaim` |
| When | After outer parts selected; soft-ensure from later stages |
| Send | GEN.12 JSON |
| Receive | `Id` → `dmgCaseId` |
| Store | `dmgCaseId`, `fanavaranSync.damageCase.{driverId,vehicleKindId,insuranceCorpId,lastPayload}` |
| Consumers | Expertise payload (`DmgCaseId`) |
### D. Attachments
| | |
|--|--|
| Controller | `previewAttachments` / `submitAttachments` |
| Functions | `previewFanavaranAttachmentsV2`, `submitFanavaranAttachmentsV2`, `autoSubmitFanavaranAttachment` |
| When | After local required-doc / capture upload |
| Send | multipart GEN.07 |
| Receive | file metadata / ids |
| Store | `fanavaranSync.attachments.files[]` |
| Consumers | Manual retry of missing uploads |
### E. Expertise
| | |
|--|--|
| Controller | `previewExpertise` / `submitExpertise` |
| Functions | `previewFanavaranExpertiseV2`, `submitFanavaranExpertiseV2`, `buildFanavaranExpertisePayload`, `autoSubmitFanavaranExpertiseOnExpertReply`, `ensureFanavaranDamageCase` |
| When | Expert reply priced / factor validated |
| Send | GEN.08 JSON |
| Receive | `Id` → `expertiseId` |
| Store | `expertiseId`, `fanavaranSync.expertise.*` |
| Consumers | Claim completion reporting / history |
### F. Lookups (read)
| | |
|--|--|
| Controller | `LookupsController` |
| Service | `LookupsService` → `FanavaranLookupService` |
| When | UI dropdowns; cold cache; payload id resolution |
| Store | `files/fanavaran-lookups/{client}/*.json` |
### G. Tenant config admin
| | |
|--|--|
| Module | System Settings |
| Service | `SystemSettingsService` / `FanavaranClientConfigService` |
| When | Boot seed missing keys; admin update |
| Store | `fanavaranClientConfigs` → in-memory cache via `setFanavaranClientProfilesCache` |
### H. Claim create call sites (auto base)
`autoSubmitToFanavaranV2OnClaimCreated` is invoked from claim creation paths inside `ClaimRequestManagementService` (registrar / expert-initiated / v2 create flows — search call sites ~7717, 7839, 8141, 8243).
Outer parts selection invokes damage auto-submit (~8422).
Document/capture upload hooks call attachment auto-submit (~9496+).
Expert reply path calls expertise auto-submit (~10576).
---
## Controllers summary
| Controller | Fanavaran-related routes |
|------------|--------------------------|
| `FanavaranController` | All `/v2/fanavaran/*` preview/submit |
| `LookupsController` | `/lookups/*` Fanavaran-backed reads |
| Claim V2 / registrar / expert mirrors | Create/select/upload that **trigger** auto-submit (no direct Fanavaran URL) |
---
## Data written by Fanavaran stages
| Field path | Stage |
|------------|-------|
| `claimCases.claimId` / `claimNo` | GEN.03 |
| `claimCases.dmgCaseId` | GEN.12 |
| `claimCases.expertiseId` | GEN.08 |
| `claimCases.fanavaranSync.*` | All |
| `claimCases.history[]` | Success/fail events |
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve |
| `fanavaranAuditLogs` | Every real HTTP step |
| `fanavaranAuthTokens` | Auth cache |
| `fanavaranClientConfigs` | Tenant profiles |

View File

@@ -0,0 +1,92 @@
---
last_updated: 2026-08-08
tags: [fanavaran, mapping]
source: fanavaran-module-docs
---
# 07 — Data mapping (YARA → Fanavaran)
## GEN.03 — Base claim
| YARA | Fanavaran |
|------|-----------|
| Guilty party insurer national code → policy inquiry | `PolicyId` |
| Tenant `defaults.ClaimExpertId` | `ClaimExpertId` |
| Tenant defaults city/report/vehicle-used/… | `AccidentCityId`, `AccidentReportTypeId`, … |
| Blame/claim `createdAt` (Jalali) | `AccidentDate`, `AnnouncementDate`, `DocReceivedDate` |
| Blame/claim time | `AccidentTime` |
| Constant address | `AccidentLocationAddress` |
| Sum damage parts estimate (or 1000) | `EstimateAmount` |
| Accident reason fanavaran id (else default 6) | `AccidentCauseId` |
| Culprit licence digits / dummy | `CulpritLicenceNo` |
| Culprit licence issue / birthday fallback | `CulpritLicenceIssuDate` |
| Tenant `CulpritLicenceTypeId` / `CulpritTypeId` | same |
| — | Explicit `null` placeholders for unused template fields |
## GEN.12 — Damage case
| YARA | Fanavaran |
|------|-----------|
| `damage.selectedParts` labels | `Desc` (`/` joined) |
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry | `DriverId` |
| `claimCase.vehicle.carType` → vehicle-kinds | `VehicleKindId` |
| `INSURANCE_CORP_ID` caption → insurance-corp | `InsuranceCorpId` |
| Inquiry `ShsNum` / Chassis* | `ChassisNo` |
| Inquiry `MtrNum` / Engine* | `MotorNo` |
| Inquiry `VIN` / vin | `VIN` |
| Inquiry / plate snapshot | `PlaqueLeftNo`, `PlaqueRightNo`, `PlaqueSerial`, `PlaqueMiddleCodeId`, `PlaqueNo` |
| Inquiry policy numbers / dates | `PolicyNo`, `PolicyCINumber`, `BeginDate`, `EndDate`, `PreviousPolicyEndDate` |
| Inquiry model year | `BuiltYear` |
| `person.driverLicense` / `insurerLicense` / dummy | `LicenceNo` |
| `person.driverBirthday` | `LicenceIssuDate` |
| `person.driverIsInsurer` | `DriverIsOwner` (1/0) |
| Tenant defaults | `DmgCaseTypeId`, `DmgHistoryStatus`, `PlaqueKindId`, `PlaqueSampleId`, `FaultPercent`, `AccidentVehicleUsedId`, `LicenceTypeId` |
| Provisional | `EstimateAmount` = 1000 |
## GEN.07 — Attachments
| YARA | Fanavaran |
|------|-----------|
| Local image filename | `FileName` (+ multipart `files`) |
| Tenant `ClaimFileTypeId` | `FileTypeId` |
| Local file bytes | multipart binary |
| `claimId` | URL path |
## GEN.08 — Expertise
| YARA | Fanavaran |
|------|-----------|
| Tenant `ExpertiseClaimExpertId` | `ClaimExpertId` |
| `claimCase.dmgCaseId` | `DmgCaseId` |
| Expert reply parts `salary` (sum) | `RepairWage` |
| Expert reply parts `price` (sum) | `ComponentReplacementCost` |
| Part `daghi.price` (sum) | `WasteValue` |
| Reply `submittedAt` | `DmgAssessmentDate`, `InspectionTime` |
| `evaluation.priceDrop.total` | `DropAmountAdditionsDeductions` |
| `evaluation.priceDrop.carPrice` | `DamagedVehicleCurrentPrice` |
| Part catalog / `partId` | `DmgSections[].DmgSectionId` |
| Part `typeOfDamage` / default level | `DmgSections[].AccidentLevel` |
| Part label / type | `DmgSections[].Desc` |
| Part line `salary` / `price` / daghi | section money fields |
| Code constants today | `InspectionPlaceId`, `DropAmountStatus` |
## Auth headers
| YARA profile.auth | Fanavaran header |
|-------------------|------------------|
| `corpId` | `CorpId` |
| `contractId` | `ContractId` |
| `location` | `Location` |
| token from Login | `authenticationToken` |
## Not mapped / out of scope
| YARA | Note |
|------|------|
| `CAR_BODY` claims | No Fanavaran submit |
| Videos | Not uploaded via GEN.07 in current code |
| Full police report fields | Often null unless later enriched |
## Alias notes (inquiry)
ESG / Tejarat inquiry payloads use multiple key names; builders try ordered aliases (e.g. `PrntCmpDocNo` / `printNumber` / `insuranceNumber` for policy number). See `pickPartyInquiryField` in claim service.

View File

@@ -0,0 +1,45 @@
---
last_updated: 2026-08-08
tags: [fanavaran, dependencies]
source: fanavaran-module-docs
---
# 08 — System dependencies
## Required
| Dependency | Role |
|------------|------|
| **MongoDB** | `claimCases`, `blameCases` / request-management, `fanavaranAuthTokens`, `fanavaranAuditLogs`, `fanavaranClientConfigs`, claim documents refs |
| **Outbound HTTPS** | Fanavaran API Manager (`apimanager.iraneit.com`) |
| **Filesystem** | `files/fanavaran-lookups/{client}/`, uploaded claim media for GEN.07 |
| **Env / config** | `FANAVARAN_CLIENT`, `INSURANCE_CORP_ID`, optional `FANAVARAN_DUMMY_LICENCE_NO`, SMS provider env |
| **Nest HttpModule** | Axios via `HttpService` |
| **Logging** | Nest `Logger` + audit collection |
## Optional / related
| Dependency | Role |
|------------|------|
| **SMS provider** | Notify owner after successful GEN.08 expertise (`SmsOrchestrationService`; same as login SMS stack) |
| **Offline inquiry seeds** | Can supply driver/policy test data without live inquiry |
| **Redis / Queue** | **Not** used for Fanavaran stage orchestration today — retries are in-process `setTimeout` + Mongo `nextRetryAt` |
## Config sources (priority)
1. Mongo `fanavaranClientConfigs` (loaded to memory on boot)
2. Seed `SEED_FANAVARAN_CLIENT_PROFILES` if key missing (never overwrite existing DB edits on seed)
3. Env selects **which** client is active (`FANAVARAN_CLIENT`)
## Collections (Fanavaran-specific)
| Collection | Purpose |
|------------|---------|
| `fanavaranClientConfigs` | Per-tenant auth + defaults |
| `fanavaranAuthTokens` | Shared token until Tehran midnight |
| `fanavaranAuditLogs` | Request/response audit (secrets masked, bodies truncated ~80KB) |
| `claimCases.fanavaranSync` | Stage state machine |
## File storage
Local claim images/documents must be readable by the process for multipart upload. Failures on missing files are recorded on the attachments stage without blocking the user journey.

View File

@@ -0,0 +1,72 @@
---
last_updated: 2026-08-08
tags: [fanavaran, errors, retry]
source: fanavaran-module-docs
---
# 09 — Error handling
## Principles
1. Fanavaran failures must **not** abort the local user claim flow on auto-submit.
2. Persist warning + history + `fanavaranSync.*.lastError`.
3. Allow manual retry via `/v2/fanavaran/.../submit`.
4. Audit only real Fanavaran HTTP (warm cache hits are silent).
---
## Common errors
| دلیل | پیام نمونه | مدیریت | Retry | Log | اطلاع‌رسانی |
|------|------------|--------|-------|-----|-------------|
| Wrong login / appToken | `نام کاربر یا رمز عبور صحیح نیست` | Fix credentials; ensure fresh GetAppToken before Login | After fix | Auth audit | Ops |
| Transient overload | `لطفا پس از چند لحظه مجدد تلاش فرمایید` | Tenant-wide backoff 5 min; do **not** immediately re-auth storm | Delayed (~5 min) | Auth + stage | Ops via audit |
| Missing PolicyId | No/expired policies messages | Block submit; preview may leave null | Manual after data fix | Stage fail history | Admin |
| Duplicate create | Local `claimId`/`claimNo` exists | Skip create; use follow-ups | N/A | skipReason | — |
| Invalid FileTypeId | `نوع فايل با منبع لوکاپ مطابقت ندارد` | Set `ClaimFileTypeId` from tenant `file-types` (Parsian **63**) | After config fix | Attachment stage | Ops |
| Expertise not ready | `Fanavaran expertise payload is not ready` + warnings | Wait for priced parts / factor | Auto when ready | — | Expert UI |
| Network / 5xx | Axios / gateway errors | Stage failed + schedule retry | Up to `maxRetries` (2) | Audit | Ops |
| Max retries exhausted | Logged warn | Manual submit only | Stop auto | Stage status | Ops |
---
## Retry mechanics
Implemented in `scheduleFanavaranRetry`:
| Rule | Behavior |
|------|----------|
| Delay (normal) | 5 minutes |
| Delay (transient try-later) | 10 minutes + `registerFailure` tenant backoff (5 min) |
| Max | Default 2 per stage (`fanavaranSync.*.maxRetries`) |
| Dedupe | In-process timer map + existing future `nextRetryAt` |
| Lock | `withFanavaranStageLock` prevents concurrent stage runs |
---
## History event types (examples)
| Event | Meaning |
|-------|---------|
| `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED` | GEN.03 ok |
| `FANAVARAN_EARLY_AUTO_SUBMIT_FAILED` | GEN.03 fail |
| `FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED` / `_FAILED` | GEN.12 |
| `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED` / `_FAILED` | GEN.08 |
| Attachment success/fail | Via sync status + audit (and related history where pushed) |
---
## Audit log (`fanavaranAuditLogs`)
Per HTTP step:
- `requestUrl`, `requestMethod`, `httpStatus`, `durationMs`
- Headers/bodies (secrets masked, truncated)
- `errorMessage` / `errorDetails` on failure
- Look for `fromCache: true` meta when PolicyId/auth reused without live call
---
## SMS
After successful expertise (last Fanavaran stage), owner SMS may be sent once (`fanavaranSync.expertise.smsNotifiedAt`). Failure to SMS should not roll back Fanavaran expertise submit; treat as separate notification concern. Claims already notified at base claim (`fanavaranSync.baseClaim.smsNotifiedAt`) are not re-notified.

View File

@@ -0,0 +1,180 @@
---
last_updated: 2026-08-17
tags: [fanavaran, testing]
source: fanavaran-module-docs
---
# 10 — Testing
## Prerequisites
1. `FANAVARAN_CLIENT=parsian` (or target tenant)
2. Valid tenant credentials in Mongo / seed
3. `INSURANCE_CORP_ID` matching that tenant’s insurance-corp Caption
4. Network access to `apimanager.iraneit.com`
5. Optional: warm lookups via first `GET /lookups/fanavaran/{name}` or auth script
## Auth smoke test
```bash
scripts/fanavaran-auth.sh parsian
source files/fanavaran-auth/parsian/tokens.env
# then curl a lookup — see docs/external-api-curls.md
```
## Manual Fanavaran flow test (no YARA)
Use this when there is **no YARA claim** and you need to prove a tenant (Parsian / Tejaratno / Moallem) against Fanavaran with the **same stage order as the app**.
Files in git:
| Path | Role |
|------|------|
| `scripts/fanavaran-flow-test.sh` | Standalone tester |
| `scripts/data/fanavaran-flow.env.example` | Template env (section A = you fill, section B = script fills) |
Do **not** commit a filled copy (`fanavaran-flow.moallem.env` and similar). It contains national codes, plate data, and secrets. Copy the example on the machine that runs the test.
### 1. Copy and fill the env
```bash
cp scripts/data/fanavaran-flow.env.example scripts/data/fanavaran-flow.<client>.env
```
Fill **section A** before the first run:
- Tenant: `FANAVARAN_CLIENT`, optional auth overrides (`APP_NAME`, `CORP_ID`, `LOCATION`, …)
- Lookup ids **from that tenant** (`files/fanavaran-lookups/<client>/`): `CLAIM_EXPERT_ID`, `EXPERTISE_CLAIM_EXPERT_ID`, `CLAIM_FILE_TYPE_ID`, `VEHICLE_KIND_ID`, `DMG_SECTION_ID`
- `INSURANCE_CORP_ID`: Persian caption **or** numeric Fanavaran Id
- Case data: guilty national code, driver national code + Jalali birthday, plate/chassis/VIN if you have them
- `ATTACHMENT_FILE`: absolute path(s) to image(s) **on the host that runs the script** (comma-separated for several files)
- Leave **section B empty** (`POLICY_ID`, `CLAIM_ID`, …)
Lookup ids from Tejaratno/Parsian files are invalid for Moallem (Fanavaran returns `نوع خودرو یافت نشد` and similar). Fetch Moallem lookups first (`GET /lookups/vehicle-kinds` with `FANAVARAN_CLIENT=moallem`, or curl Fanavaran with that tenant’s token).
### 2. Network / IP whitelist
Fanavaran Login is IP-restricted. From a **whitelisted server**, run with no proxy. From **localhost**, Termius dynamic port forwarding does **not** apply automatically — set:
```env
CURL_PROXY=socks5h://127.0.0.1:<termius-socks-port>
```
Error `کاربر … مجاز به لاگین با آی پی … نمیباشد` means curl is still using your home IP.
### 3. Run (same sequence as the app)
```text
auth (cached until Tehran midnight)
→ policy inquiry → GEN.03 base
→ driver inquiry + insurance-corp
→ GEN.12 damage
→ GEN.07 attachments (one request per file)
→ GEN.08 expertise
```
From repo root (`bash`, `curl`, `node`, `awk` required):
```bash
chmod +x scripts/fanavaran-flow-test.sh
# Full flow (confirms before each POST)
./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env
# One or more stages
./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages base,damage
./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages attachments
./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages expertise
# Build payloads / inquiries only
./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --preview-only
```
Missing section-A fields can be typed when prompted; they are written back into the env file.
### 4. Token cache (do not Login every run)
`authenticationToken` is stored in `files/fanavaran-auth/<client>/tokens.env` until **Asia/Tehran midnight** (same as Nest). Later runs print `Reusing cached authenticationToken`. Use `--force-login` only when you must mint a new token.
Repeated Login causes Fanavaran `لطفا پس از چند لحظه مجدد تلاش فرمایید`.
### 5. Resume after a stage succeeds
Section B is updated in the **same env file**. Re-run; stages with `CLAIM_ID` / `DMG_CASE_ID` already set are skipped (soft-skip). Payloads and HTTP bodies also land under `files/fanavaran-flow/<client>/<timestamp>/` (gitignored).
### 6. Typical failures
| Message | What to do |
|---------|------------|
| Login IP not allowed | Run on the tenant server, or set `CURL_PROXY` to Termius SOCKS |
| Try again later | Wait; reuse cache; do not `--force-login` |
| `نوع خودرو یافت نشد` | Set `VEHICLE_KIND_ID` from **this** tenant’s `vehicle-kinds` lookup |
| File type lookup mismatch | Set `CLAIM_FILE_TYPE_ID` from this tenant’s `file-types` |
| No `authenticationToken` | Read `login.body.json` in the run folder — Fanavaran `Message` is the real error |
## Unit / service specs
| Spec | Focus |
|------|-------|
| `src/fanavaran/fanavaran-auth.service.spec.ts` | Token cache / midnight / backoff |
| `src/claim-request-management/fanavaran-policy-selection.spec.ts` | Latest active policy selection |
Prefer mocks for Fanavaran HTTP in unit tests; use live calls only in controlled integration.
## Manual E2E (Parsian template — via YARA)
1. Create THIRD_PARTY claim with guilty party national code that has an **active** Fanavaran policy.
2. Confirm history `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED` and `claimId`/`claimNo`.
3. Select outer parts → `dmgCaseId` set.
4. Upload images → attachment file ids under `fanavaranSync.attachments`.
5. Submit expert pricing → `expertiseId`.
6. Cross-check `fanavaranAuditLogs` for each stage.
Preview first if debugging:
```http
GET /v2/fanavaran/parsian/claim-cases/{id}/base-claim/preview?debug=true
GET /v2/fanavaran/parsian/claim-cases/{id}/damage-case/preview
GET /v2/fanavaran/parsian/claim-cases/{id}/attachments/preview
GET /v2/fanavaran/parsian/claim-cases/{id}/expertise/preview
```
## Success scenarios
| Scenario | Expect |
|----------|--------|
| Happy path Parsian | All four stages succeed |
| Re-preview after PolicyId cached | No new policy inquiry |
| Soft-ensure | Damage submit creates base if missing |
| Skip duplicate base | Second auto-submit skipped when claimId exists |
## Error scenarios
| Scenario | Expect |
|----------|--------|
| Expired policy | Clear BadRequest; no create |
| Wrong FileTypeId (e.g. 70 on Parsian) | Attachment fail message about lookup |
| Transient try-later | Backoff; delayed retry; no login storm |
| Fanavaran down on auto-submit | Local claim continues; history FAILED; manual retry path in warning |
## Proven reference case (Parsian)
| Local | Fanavaran |
|-------|-----------|
| `CL68535` / `A00153` / `_id` `6a707a3a8f4c6fbd851cfa75` | — |
| Base | `claimId=4909952`, `claimNo=1632`, `policyId=13764408` |
| Damage | `dmgCaseId=427594`, `DriverId=2426953` |
| Attachments | FileTypeId **63**; file ids `4629737`… |
| Expertise | `expertiseId=403144`, assessor `29` |
Defaults used: GEN.03 expert `154`, GEN.08 `29`, Location `210050`, InsuranceCorpId `329`, VehicleKindId `6704`.
## Mocks
- Mock `HttpService` / axios for auth + submit in unit tests.
- Offline plate/driver seeds for local payload builds without live inquiry (used in proven Parsian offline+live mix).
- Do not commit live tokens.
## Curl catalogue
Operational curl sequences (auth, lookups, sample submits): [`docs/external-api-curls.md`](../external-api-curls.md).

View File

@@ -0,0 +1,76 @@
---
last_updated: 2026-08-08
tags: [fanavaran, onboarding, checklist]
source: fanavaran-module-docs
---
# 11 — Onboard a new insurance client
Use **Parsian** as the behavioral template. API shapes stay the same; only tenant config + lookup ids change.
## Checklist
### 1. Register tenant key
- [ ] Add key to `FanavaranClientKey` / `FANAVARAN_CLIENT_KEYS` in `fanavaran-client.config.ts`
- [ ] Add `SEED_FANAVARAN_CLIENT_PROFILES[key]` with auth + defaults
- [ ] Boot app once so `FanavaranClientConfigService` seeds Mongo if missing
### 2. Auth verification
- [ ] `scripts/fanavaran-auth.sh <key>` succeeds
- [ ] Login returns `authenticationToken`
- [ ] Business call with CorpId/ContractId/Location succeeds (any small lookup)
### 3. Warm lookups
- [ ] Fetch `file-types`, `vehicle-kinds`, `insurance-corp`, `car-components`, `inspection-place`, `accident-level`, expert-related lists
- [ ] Confirm cache under `files/fanavaran-lookups/<key>/`
### 4. Choose tenant-specific ids
| Field | How to pick |
|-------|-------------|
| `ClaimExpertId` (GEN.03) | Financial case owner role for that insurer |
| `ExpertiseClaimExpertId` (GEN.08) | Assessor role — **different** from GEN.03 |
| `ClaimFileTypeId` | Must exist in **that** tenant’s `file-types.json` |
| Shared codebook defaults | Start from `SHARED_FANAVARAN_DEFAULTS`; override if Fanavaran rejects |
### 5. Deploy env
```bash
FANAVARAN_CLIENT=<key>
INSURANCE_CORP_ID='<exact Caption from insurance-corp lookup>'
```
### 6. Dry-run on a test claim
- [ ] Preview base → PolicyId resolves
- [ ] Submit base → `claimId`/`claimNo`
- [ ] Outer parts → `dmgCaseId`
- [ ] Upload → attachments with correct FileTypeId
- [ ] Expertise → `expertiseId`
- [ ] Review `fanavaranAuditLogs` and history events
### 7. Document deltas
- [ ] Add a short section under [04-tenant-matrix.md](./04-tenant-matrix.md) for the new key
- [ ] Note any non-Parsian business rules (validation, required fields)
## Do / Don’t
| Do | Don’t |
|----|-------|
| Copy orchestration from existing code | Copy Parsian expert/file-type ids blindly |
| Verify FileTypeId in tenant lookup | Use template sample `70` without checking |
| Keep null fields in payloads | Strip nulls from GEN.03/12 templates |
| Soft-fail auto-submit | Block user UX on Fanavaran outage |
## Acceptance for “docs-only onboarding”
A backend engineer should be able to complete the checklist above using only:
1. This docs set (`docs/fanavaran/`)
2. `docs/external-api-curls.md`
3. Seed/config + System Settings for credentials
4. Parsian proven values as the reference baseline

101
docs/fanavaran/README.md Normal file
View File

@@ -0,0 +1,101 @@
---
last_updated: 2026-08-17
tags: [fanavaran, documentation, parsian, third-party-claim]
source: fanavaran-module-docs
---
# Fanavaran Integration — Technical Reference
مرجع فنی یکپارچه‌سازی یارا با سرویس‌های فناوران برای ثبت خسارت مالی ثالث خودرو.
**Template tenant:** `parsian` (proven end-to-end, 2026-08-03 — claim `CL68535` / publicId `A00153`)
**Also configured:** `tejaratno` (production shape), `moallem` (auth seeded; expert/file-type ids TBD)
## Goals
1. Document every Fanavaran API used in claim registration
2. Clarify per-insurer dependencies
3. Identify reusable pieces
4. Map YARA ↔ Fanavaran integration points
5. Enable onboarding a new insurer from this docs set alone
## Document index
| # | Document | Covers |
|---|----------|--------|
| 1 | [01-write-apis.md](./01-write-apis.md) | Auth + GEN.03 / GEN.12 / GEN.07 / GEN.08 write APIs |
| 2 | [02-read-apis-lookups.md](./02-read-apis-lookups.md) | Policy inquiry, driver inquiry, lookups |
| 3 | [03-constants.md](./03-constants.md) | Shared vs tenant constants |
| 4 | [04-tenant-matrix.md](./04-tenant-matrix.md) | Shared ✅ / tenant-specific ✅ matrix |
| 5 | [05-claim-flow.md](./05-claim-flow.md) | Full claim flow + sequence diagrams |
| 6 | [06-yara-integration.md](./06-yara-integration.md) | Module / service / controller / function map |
| 7 | [07-data-mapping.md](./07-data-mapping.md) | YARA field → Fanavaran field |
| 8 | [08-dependencies.md](./08-dependencies.md) | Mongo, files, env, SMS, audit |
| 9 | [09-error-handling.md](./09-error-handling.md) | Errors, retry, backoff, logging |
| 10 | [10-testing.md](./10-testing.md) | Test scenarios, **manual flow-test script**, proven case |
| 11 | [11-onboard-new-client.md](./11-onboard-new-client.md) | Checklist to add a new insurer |
## Related sources (code)
| Path | Role |
|------|------|
| `src/core/config/fanavaran-client.config.ts` | Tenant keys, seed auth + defaults |
| `src/fanavaran/` | Auth, lookup, audit, YARA HTTP surface |
| `src/claim-request-management/claim-request-management.service.ts` | Payload build + staged submit orchestration |
| `src/claim-request-management/fanavaran-policy-selection.ts` | Latest active policy selection |
| `src/lookups/` | Local lookup HTTP façade over Fanavaran |
| `.agents/skills/fanavaran-apis/references/third-party-cases.md` | Agent-oriented implementation rules |
| `docs/external-api-curls.md` | Ready-to-run curl sequences |
| `scripts/fanavaran-auth.sh` | Token helper |
| `scripts/fanavaran-flow-test.sh` | Manual Fanavaran-only staged submit (see [10-testing.md](./10-testing.md)) |
| `scripts/data/fanavaran-flow.env.example` | Env template for the flow-test script |
## Architecture (one glance)
```text
YARA claim flow (THIRD_PARTY only)
│
├─ auto / manual stage triggers
│
▼
ClaimRequestManagementService
├─ FanavaranAuthService (GetAppToken → Login, cache until Tehran midnight)
├─ FanavaranLookupService (lookups + insurance-corp resolve)
├─ FanavaranAuditService (fanavaranAuditLogs)
└─ FanavaranClientConfigService (Mongo fanavaranClientConfigs)
│
▼
Fanavaran API Manager
https://apimanager.iraneit.com/BimeApiManager/api
```
## Stages (order)
| Stage | Fanavaran doc | Local ids stored |
|-------|---------------|------------------|
| Auth | EITAuthentication | token cache (`fanavaranAuthTokens`) |
| Policy inquiry | inquiry-my-policies | `fanavaranSync.baseClaim.policyId` |
| Base claim | GEN.03 | `claimId`, `claimNo` |
| Damage case | GEN.12 | `dmgCaseId`, `driverId`, … |
| Attachments | GEN.07 | `fanavaranSync.attachments.files[]` |
| Expertise | GEN.08 | `expertiseId` |
Soft-ensure: later stages create earlier ones if missing (damage → base; expertise → damage → base).
## Secrets policy
This documentation **does not** embed live secrets. Auth values (`appName`, `secret`, `username`, `password`) live in:
1. Mongo collection `fanavaranClientConfigs` (runtime source of truth after boot seed)
2. Seed fallback: `SEED_FANAVARAN_CLIENT_PROFILES` in `fanavaran-client.config.ts`
Use `scripts/fanavaran-auth.sh <client>` or System Settings admin APIs to inspect/update tenant config.
## Activate a tenant
```bash
FANAVARAN_CLIENT=parsian # or tejaratno | moallem
INSURANCE_CORP_ID='...' # Persian caption matching Fanavaran insurance-corp lookup
```
Optional: `CLIENT_ID=8` maps to `parsian` when `FANAVARAN_CLIENT` is unset.

View File

@@ -0,0 +1,625 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<title>مرجع نقش‌های پنل</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Vazirmatn", "Tahoma", "Segoe UI", system-ui, sans-serif;
font-size: 14px;
line-height: 1.8;
background: #ffffff;
color: #1f2328;
padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.7;
}
/* Role header strip */
.role-header {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 6px;
}
.role-name {
font-size: 15px;
font-weight: 700;
}
.role-enum {
font-family: monospace;
font-size: 11px;
color: #3b82d4;
background: #f0f7ff;
border: 1px solid #bfdbfe;
border-radius: 4px;
padding: 1px 6px;
direction: ltr;
unicode-bidi: embed;
}
.badge {
display: inline-block;
font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
margin-left: 4px; margin-bottom: 3px;
}
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #dcfce7; color: #166534; }
.badge-purple { background: #ede9fe; color: #5b21b6; }
.badge-orange { background: #ffedd5; color: #9a3412; }
.badge-teal { background: #ccfbf1; color: #0f766e; }
.badge-indigo { background: #e0e7ff; color: #3730a3; }
.badge-gray { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.badge-red { background: #fee2e2; color: #991b1b; }
/* Cards */
.card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
background: #f7f8fa;
margin-bottom: 16px;
}
.card.card-blue { border-right: 4px solid #3b82f6; }
.card.card-green { border-right: 4px solid #22c55e; }
.card.card-purple { border-right: 4px solid #8b5cf6; }
.card.card-orange { border-right: 4px solid #f97316; }
.card.card-teal { border-right: 4px solid #14b8a6; }
.card.card-indigo { border-right: 4px solid #6366f1; }
.card.card-gray { border-right: 4px solid #94a3b8; }
.card.card-red { border-right: 4px solid #ef4444; }
/* Endpoint tables */
table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
margin-top: 4px;
direction: rtl;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: right; padding: 5px 8px;
border: 1px solid #e5e7eb;
}
td {
padding: 4px 8px; border: 1px solid #e5e7eb;
vertical-align: top;
}
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
direction: ltr; unicode-bidi: embed;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
direction: ltr; unicode-bidi: embed;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.method.delete { color: #dc2626; }
.dep { color: #94a3b8; font-style: italic; font-size: 11px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }
.note {
font-size: 11px; color: #57606a; font-style: normal;
margin-top: 6px;
}
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px;
margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-right: 18px; padding-left: 0; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb;
margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
/* Role overview table */
.overview-table { font-size: 12px; margin-bottom: 24px; }
.overview-table th { white-space: nowrap; }
.overview-table td:last-child { font-weight: 600; white-space: nowrap; }
</style>
</head>
<body>
<div class="max-wrap">
<h1>مرجع نقش‌های پنل</h1>
<p class="subtitle">
آنچه هر نقش می‌تواند ببیند و انجام دهد — اندپوینت‌ها، مسئولیت‌ها و
مراحل فرآیند. سوپر ادمین در این مستند نیست.
</p>
<!-- Table of Contents -->
<div class="toc">
<div class="toc-title">نقش‌های پوشش‌داده‌شده</div>
<ol>
<li><a href="#insurer">بیمه‌گر (COMPANY) — ادمین تنانت شرکت بیمه</a></li>
<li><a href="#blame-expert">کارشناس تقصیر (EXPERT) — صف بررسی اختلاف</a></li>
<li><a href="#damage-expert">کارشناس خسارت (DAMAGE_EXPERT) — قیمت‌گذاری خسارت</a></li>
<li><a href="#field-expert">کارشناس میدانی (FIELD_EXPERT) — ثبت حضوری در صحنه</a></li>
<li><a href="#file-maker">فایل‌ساز (FILE_MAKER) — روایت طرفین در V4/V5</a></li>
<li><a href="#file-reviewer">بازبین فایل (FILE_REVIEWER) — ارزیابی خسارت V4/V5</a></li>
<li><a href="#registrar">ثبات (REGISTRAR) — ثبت اداری حضوری</a></li>
<li><a href="#call-center">مرکز تماس (CALL_CENTER) — ثبت تلفنی V6</a></li>
</ol>
</div>
<!-- ── Role overview table ────────────────────────────────────── -->
<h2>نمای کلی نقش‌ها</h2>
<table class="overview-table">
<tr>
<th>وظیفه اصلی</th>
<th>محدوده</th>
<th>پنل ورود</th>
<th>enum نقش</th>
</tr>
<tr>
<td>مشاهده تمام فایل‌ها؛ مدیریت شعب و کارشناسان؛ گزارش‌گیری؛ امتیازدهی به کارشناسان</td>
<td>سطح تنانت</td>
<td>پورتال بیمه‌گر</td>
<td><code>company</code></td>
</tr>
<tr>
<td>قفل‌کردن پرونده‌های تقصیر، بررسی اسناد طرفین، صدور رأی یا درخواست ارسال مجدد</td>
<td>صف DISAGREEMENT تنانت</td>
<td>پنل کارشناس تقصیر</td>
<td><code>expert</code></td>
</tr>
<tr>
<td>قفل‌کردن خسارت، قیمت‌گذاری، اعتبارسنجی فاکتورها، درخواست ارسال مجدد/بازدید</td>
<td>صف خسارت تنانت</td>
<td>پنل خسارت</td>
<td><code>damage_expert</code></td>
</tr>
<tr>
<td>ثبت حضوری تقصیر + خسارت در V2/V3؛ دسترسی به پنل‌های تقصیر/خسارت</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل کارشناس میدانی</td>
<td><code>field_expert</code></td>
</tr>
<tr>
<td>روایت طرفین V4/V5 (OTP، استعلام، جزئیات، امضا)؛ تأیید خسارت در V5</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل فایل‌ساز</td>
<td><code>file_maker</code></td>
</tr>
<tr>
<td>ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات و عکس‌ها؛ تأیید خطوط قیمت‌گذاری‌شده در جریان ترکیبیِ فاکتور در صورت نیاز)</td>
<td>فایل‌های تخصیص‌یافته</td>
<td>پنل بازبین فایل</td>
<td><code>file_reviewer</code></td>
</tr>
<tr>
<td>ثبت حضوری اداری تقصیر + خسارت به نمایندگی از طرفین</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل ثبات</td>
<td><code>registrar</code></td>
</tr>
<tr>
<td>ثبت تلفنی V6: اجرای استعلام، ارسال لینک؛ کاربر بقیه را تکمیل می‌کند</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل مرکز تماس</td>
<td><code>call_center</code></td>
</tr>
</table>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="insurer">۱ — بیمه‌گر <span class="role-enum">company</span></h2>
<p class="section-intro">
به ازای هر تنانت شرکت بیمه یک اکتور <code>company</code> وجود دارد. پورتال بیمه‌گر
لایه مدیریتی است: می‌تواند همه چیز زیر تنانت خود را ببیند، لیست کارشناسان را مدیریت
کند، شعب را اداره کند، تنظیمات رسانه‌ای هر تنانت را پیکربندی کند و گزارش‌های آماری
استخراج کند. بیمه‌گر هرگز مستقیماً با مراحل تقصیر/خسارت درگیر نمی‌شود — فقط نظاره‌گر
و امتیازدهنده است.
</p>
<div class="card card-blue">
<h3>مدیریت فایل — <code>expert-insurer/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files</code></td><td>فهرست تمام فایل‌های تقصیر + خسارت تنانت (ادغام‌شده بر اساس publicId). فیلترپذیر بر اساس وضعیت، نوع فایل، جستجو، مرتب‌سازی، صفحه.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId</code></td><td>جزئیات کامل یک فایل بر اساس publicId.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/timeline</code></td><td>تایم‌لاین فعالیت به ترتیب زمانی (تمام رویدادهای تاریخچه: منبع، نوع، اکتور، متادیتا).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/report</code></td><td>داده‌های ساختاریافته گزارش برای تولید PDF (بخش‌های مالک، راننده، بیمه، خودرو، تصادف).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/files/:publicId/rating</code></td><td>امتیازدهی به کارشناسان یک فایل (۱–۵ در هر بُعد: روش تصادف، به‌موقع‌بودن، دقت علت، دقت شناسایی مقصر، امتیاز ربات).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت یکپارچه + تعداد به ازای هر وضعیت برای کل پرتفولیوی تنانت. فیلترپذیر بر اساس fileType و بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت شعب — <code>expert-insurer/branches</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/branches</code></td><td>فهرست تمام شعب این بیمه‌گر. پارامترها: جستجو، بازه تاریخ from/to، فیلتر isActive.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/branches</code></td><td>افزودن شعبه جدید (نام، کد، آدرس، شهر، تلفن و غیره).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/branches/:branchId/status</code></td><td>فعال یا غیرفعال کردن یک شعبه.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت لیست کارشناسان — <code>expert-insurer/experts</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/blame</code></td><td>ایجاد حساب کارشناس تقصیر جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/claim</code></td><td>ایجاد حساب کارشناس خسارت جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-maker</code></td><td>ایجاد حساب فایل‌ساز جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-reviewer</code></td><td>ایجاد حساب بازبین فایل جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/list</code></td><td>فهرست صفحه‌بندی‌شده تمام حساب‌های کارشناس در این تنانت.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/top</code></td><td>برترین کارشناسان تقصیر و خسارت رتبه‌بندی‌شده بر اساس میانگین امتیاز کلی (حداکثر ۱۰ نفر از هر نوع).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-experts</code></td><td>نام مستعار experts/top (سازگاری با فرانت‌اند).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/:expertId</code></td><td>فایل‌های رسیدگی‌شده توسط یک کارشناس (ردیف‌های خلاصه — تقصیر یا خسارت بسته به نوع کارشناس).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>آمار و گزارش‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/statistics</code></td><td>کارت‌های KPI: totalFilesReviewed، averageUserRating، inPersonCount، filesThisMonth، objectionPercentage و غیره. فیلترپذیر بر اساس بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-files</code></td><td>۱۰ فایل خسارت برتر بر اساس بالاترین امتیاز (ترکیبی از امتیاز بیمه‌گر + کاربر).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/expert-work-log</code></td><td>لاگ کاری هر کارشناس: totalHandled، currentlyChecking، distinctFilesCheckedInPeriod. فیلترپذیر بر اساس expertKind و بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/requests</code></td><td>تعداد خسارت + وضعیت تقصیر + تعداد فایل یکپارچه برای تنانت.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/per-month-requests</code></td><td>همان خلاصه، تفکیک‌شده بر اساس ۵ ماه تقویمی اخیر.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/checked-requests</code></td><td>همان خلاصه، فیلترشده بر اساس بازه زمانی اختیاری createdAt.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log</code></td><td>لاگ کاری کارشناسان (مجموعه‌های کارشناس تقصیر و خسارت، نه کارشناسان میدانی).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log/per-month</code></td><td>همان لاگ کاری تفکیک‌شده بر اساس ماه تقویمی (۵ ماه اخیر).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>تنظیمات تنانت — <code>client-panel/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>client-panel/settings</code></td><td>دریافت محدودیت‌های رسانه‌ای هر تنانت (حداکثر بایت ویدیو/تصویر/صوت) و پنجره زمانی تصادف CAR_BODY (روز).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>client-panel/settings</code></td><td>به‌روزرسانی جزئی این تنظیمات. نمی‌تواند از سقف‌های سطح سیستم تجاوز کند.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="blame-expert">۲ — کارشناس تقصیر <span class="role-enum">expert</span></h2>
<p class="section-intro">
فایل‌های تقصیر در صف DISAGREEMENT را بررسی می‌کند — پرونده‌هایی که دو طرف درباره
مقصر بودن توافق ندارند. پس از بررسی اسناد و اظهارات طرفین، کارشناس پرونده را قفل
می‌کند، سپس یا رأی صادر می‌کند، درخواست ارسال مجدد اسناد می‌دهد، یا نتیجه بازدید
حضوری را ثبت می‌کند. تمام اندپوینت‌ها زیر <code>v2/expert-blame/</code> هستند.
</p>
<div class="card card-orange">
<h3>فرآیند</h3>
<p class="note">
۱ مرور فهرست ← ۲ تخصیص (قفل) پرونده ← ۳ بررسی مدارک طرفین (ویدیو، صدا، اسناد) ←
۴الف صدور رأی <em>یا</em> ۴ب درخواست ارسال مجدد اسناد <em>یا</em> ۴پ ثبت بازدید حضوری.
</p>
</div>
<div class="card card-orange">
<h3>اندپوینت‌ها — <code>v2/expert-blame/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/</code></td><td>فهرست پرونده‌های تقصیر در صف DISAGREEMENT (موجود، قفل‌شده توسط من، یا تصمیم‌گرفته‌شده توسط من). پارامترها: search، sortBy، sortOrder، page، limit، unifiedStatus، fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/:id</code></td><td>جزئیات کامل یک پرونده تقصیر (اظهارات، عکس، صدا، ویدیو طرفین).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-blame/:id/assign</code></td><td>بررسی در دسترس بودن و قفل پرونده برای این کارشناس. بازمی‌گرداند: <code>assigned</code>، <code>already_assigned_to_you</code>، یا ۴۰۹ در صورتی که شخص دیگری آن را نگه داشته باشد.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/submit/:id</code></td><td>ارسال رأی (accidentWay، accidentReason، accidentType، تصمیم طرف مقصر). پرونده را آزاد می‌کند و به COMPLETED منتقل می‌کند.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/resend/:id</code></td><td>درخواست از طرفین برای بارگذاری مجدد اسناد. تقصیر را به WAITING_FOR_RESEND تنظیم می‌کند. یک درخواست ارسال مجدد در هر چرخه عمر.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/inPerson/:id</code></td><td>ثبت اینکه بازدید حضوری انجام شده و صدور رأی.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت + تعداد به ازای هر وضعیت برای پرتفولیوی این کارشناس.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/lock/:id</code></td><td class="dep">اندپوینت قفل منسوخ‌شده — از POST assign استفاده کنید.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="damage-expert">۳ — کارشناس خسارت <span class="role-enum">damage_expert</span></h2>
<p class="section-intro">
فایل‌های خسارت را پس از ارسال مدارک خسارت توسط کاربر بررسی می‌کند. کارشناس
هر قطعه آسیب‌دیده را قیمت‌گذاری می‌کند، به‌صورت اختیاری کاهش قیمت (استهلاک)
محاسبه می‌کند، و می‌تواند از کاربر بخواهد مدارک را مجدداً ارسال کند، حضوری مراجعه
کند، یا فاکتورهای تعمیرگاه را هنگام نیاز به قیمت‌گذاری کارگاهی بارگذاری کند.
تمام اندپوینت‌ها زیر <code>v2/expert-claim/</code> هستند.
</p>
<div class="card card-red">
<h3>فرآیند</h3>
<p class="note">
۱ مرور فهرست ← ۲ تخصیص (قفل) خسارت ← ۳ بررسی عکس‌ها و اسناد خسارت ←
۴ ویرایش اختیاری قطعات انتخاب‌شده یا محاسبه کاهش قیمت ←
۵الف ارسال پاسخ قیمت‌گذاری‌شده <em>یا</em> ۵ب درخواست ارسال مجدد <em>یا</em> ۵پ درخواست بازدید حضوری ←
۶ در صورت وجود قطعات فاکتوردار: اعتبارسنجی فاکتورهای تعمیرگاه بارگذاری‌شده.
</p>
</div>
<div class="card card-red">
<h3>اندپوینت‌ها — <code>v2/expert-claim/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/requests</code></td><td>فهرست خسارت‌ها در صف <code>WAITING_FOR_DAMAGE_EXPERT</code> + صف اعتبارسنجی فاکتور. پارامترها: search، sortBy، page، limit، unifiedStatus، fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId</code></td><td>جزئیات کامل خسارت: قطعات آسیب‌دیده، تصاویر گرفته‌شده، اسناد، priceDrop، داده طرف بلیم، آدرس‌های ویدیو.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>قفل خسارت برای این کارشناس. بازمی‌گرداند: <code>assigned</code>، <code>already_assigned_to_you</code>، یا ۴۰۹.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>محتوای کاهش قیمت: برچسب‌های شدت، کاتالوگ ضریب، قطعات آسیب‌دیده + نگاشت، سال پیشنهادی خودرو از استعلام تقصیر.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>محاسبه و ذخیره کاهش قیمت: قیمت خودرو × ضریب سال × مجموع ضرایب ÷ ۴۰۰.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>ارسال پاسخ ارزیابی خسارت (لیست قطعات قیمت‌گذاری‌شده، داغی، branchId). سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان. بسته به پرچم‌های factorNeeded، خسارت را به owner-sign، mixed-factors-pending، یا صف اعتبارسنجی فاکتور منتقل می‌کند.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>درخواست از کاربر برای ارسال مجدد اسناد/عکس‌ها. یک ارسال مجدد در هر چرخه خسارت؛ در صورت تکمیل قبلی ۴۲۲ برمی‌گرداند.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>درخواست از کاربر برای مراجعه حضوری. خسارت را آزاد می‌کند، وضعیت claimStatus را به NEEDS_REVISION تنظیم می‌کند.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>اعتبارسنجی فاکتورهای تعمیرگاه بارگذاری‌شده. تأیید یا رد هر خط فاکتور با totalPayment. سقف برای تمام خطوط اعمال می‌شود (≤ ۵۳،۰۰۰،۰۰۰ تومان). پس از تصمیم‌گیری درباره تمام خطوط، به‌صورت خودکار تکمیل می‌شود.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/request/:claimRequestId/damaged-parts</code></td><td>ویرایش قطعات آسیب‌دیده انتخاب‌شده در حالی که خسارت توسط این کارشناس قفل است (EXPERT_REVIEWING).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/outer-parts-catalog</code></td><td>کاتالوگ قطعات بیرونی خودرو فناوران (مشترک با جریان کاربر).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/inner-parts-catalog</code></td><td>JSON ثابت کاتالوگ قطعات داخلی خودرو.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/branches</code></td><td>شعب بیمه‌گر برای تنانت این کارشناس (برای انتخاب داغی/شعبه در پیلود پاسخ).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/stream/:id/video</code></td><td>پخش ویدیوی خسارت (ویدیوی دور زدن خودرو یا ویدیوی تصادف). پارامتر: <code>query=car-capture|accident</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت + تعداد برای پرتفولیوی خسارت این کارشناس.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/lock/:claimRequestId</code></td><td class="dep">اندپوینت قفل منسوخ‌شده — از POST assign استفاده کنید.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="field-expert">۴ — کارشناس میدانی <span class="role-enum">field_expert</span></h2>
<p class="section-intro">
به صحنه تصادف می‌رود و فرم‌های هر دو طرف را حضوری پر می‌کند (جریان V2 mirror / V3).
کارشناس میدانی همچنین دسترسی خواندن به پنل‌های expert-blame و expert-claim دارد
(محدود به فایل‌های خودش). تنها نقشی است که هم <strong>ثبت تقصیر</strong> و هم
<strong>ثبت خسارت</strong> را در یک جلسه انجام می‌دهد.
</p>
<div class="card card-green">
<h3>ثبت تقصیر — <code>v2/expert-initiated/blame-request-management/</code></h3>
<p class="note">آینه‌ای از API تقصیر کاربر. فرانت‌اند همان صفحات را با تغییر فقط پیشوند مسیر بازاستفاده می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code></td><td>ارسال OTP به یک طرف از طریق شماره تلفن (بدون لینک دعوت).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>verify-party-otp/:id</code></td><td>تأیید OTP یک طرف و اتصال حساب آنها.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>blame-confession/:id</code></td><td>ثبت اعتراف تقصیر طرف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[فقط CAR_BODY] فرم نوع تصادف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>فرم اولیه / استعلام پلاک یا VIN برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:id</code></td><td>بارگذاری ویدیوی طرف اول.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code></td><td>افزودن موقعیت GPS برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-voice/:id</code></td><td>بارگذاری ضبط صوتی برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-description/:id</code></td><td>افزودن توضیحات برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-second-party/:phone/:id/</code></td><td>پیشروی به طرف دوم (بدون ارسال لینک SMS).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>بارگذاری امضای طرف (اول سپس دوم، پارامتر partyRole).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>ذخیره فیلدهای تصادف و تکمیل فوری تقصیر (بدون صف کارشناس).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>ثبت تقصیر + خسارت V3 — <code>v3/expert-initiated/blame-request-management/</code></h3>
<p class="note">ترتیب مراحل بازسازمان‌دهی‌شده: ابتدا تمام روایت طرفین، سپس ارزیابی خسارت. هم تقصیر هم خسارت در این کنترلر واحد مدیریت می‌شوند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code> ← <code>send-party-otp</code> ← <code>verify-party-otp</code> ← <code>run-inquiries</code> ← <code>add-detail-*</code> ← <code>sign</code> (×۲)</td><td>مرحله روایت طرفین (مراحل ۱–۸) — اندپوینت‌های یکسان با mirror، همان قرارداد.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>مرحله ۹: ذخیره فیلدهای تصادف پس از امضای هر دو طرف.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>مرحله ۱۰: دریافت شناسه خسارت ایجادشده به‌صورت خودکار.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>مرحله ۱۱: بارگذاری اسناد گواهینامه / کارت خودرو.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code> / <code>select-other-parts/:claimId</code></td><td>مراحل ۱۲–۱۳: انتخاب قطعات آسیب‌دیده.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>مرحله ۱۴: عکس‌برداری از قطعات + زوایا.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>مرحله ۱۵: ویدیوی دور زدن خودرو.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td>مرحله ۱۶: ویدیوی تصادف تقصیر (نهایی) ← WAITING_FOR_EXPERT (THIRD_PARTY) یا COMPLETED (CAR_BODY).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>دسترسی به پنل expert-blame + expert-claim (خواندن + اقدام روی فایل‌های خود)</h3>
<p class="note">
FIELD_EXPERT مسیر <code>v2/expert-blame/</code> را محدود به فایل‌های ساخته‌شده توسط خودش می‌بیند (نه صف اختلاف).
همچنین <code>v2/expert-claim/</code> را برای خسارت‌های مرتبط با فایل‌های تقصیرش می‌بیند.
اندپوینت‌های یکسان با پنل‌های کارشناس تقصیر و کارشناس خسارت در بالا.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-maker">۵ — فایل‌ساز <span class="role-enum">file_maker</span></h2>
<p class="section-intro">
اولین اکتور در تقسیم V4/V5. فایل‌ساز روایت طرفین را در محل انجام می‌دهد:
OTPها، استعلام‌ها، موقعیت/توضیحات/صدا و امضاها برای هر دو طرف.
همچنین اسناد اولیه خسارت (گواهینامه‌ها، کارت‌های خودرو) را بارگذاری می‌کند. پس از
امضای دوم، فایل برای تحویل به بازبین فایل «مهرومومه» می‌شود. در V5، فایل‌ساز
در انتها بازمی‌گردد تا خسارت تکمیل‌شده را تأیید یا رد کند. پس از تأیید،
کارشناس پرونده را به‌صورت دستی به فناوران ارسال می‌کند.
</p>
<div class="card card-purple">
<h3>ثبت تقصیر — <code>v4/file-maker/blame-request-management/</code> و <code>v5/…</code></h3>
<p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند. V5 هنگام ایجاد <code>requiresFileMakerApproval=true</code> را تنظیم می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تقصیر ایجادشده توسط این فایل‌ساز.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل (طرفین، گردش کار، شناسه خسارت مرتبط).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار پس از استعلام طرف مقصر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code> / <code>verify-party-otp/:id</code></td><td>ارسال + تأیید OTP برای یک طرف در هر بار (ابتدا مقصر، سپس زیان‌دیده).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[فقط CAR_BODY] فرم نوع تصادف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>اجرای استعلام پلاک یا VIN. فراخوانی اول = مقصر (+ خودکار خسارت ایجاد می‌کند). فراخوانی دوم = زیان‌دیده (فقط THIRD_PARTY).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>افزودن موقعیت، توضیحات و صدا برای طرف فعلی (پارامتر partyRole، FIRST/SECOND را انتخاب می‌کند).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>بارگذاری امضای طرف (partyRole=FIRST سپس SECOND). پس از امضای دوم، فایل مهرومومه می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری گواهینامه / کارت‌های خودرو روی خسارت ایجادشده به‌صورت خودکار.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فازها: اسناد پیش از عکس‌برداری در مقابل قطعات آسیب‌دیده + شاسی/موتور).</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>تأیید خسارت V5 — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و اعتبارسنجی فاکتورهای لازم، خسارت وارد <code>WAITING_FOR_FILE_MAKER_APPROVAL</code> می‌شود؛ امضای نهایی مالک لازم نیست.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>تأیید خسارت تکمیل‌شده ← وضعیت خسارت <code>COMPLETED</code> می‌شود. کارشناس در زمان مناسب آن را به‌صورت دستی به فناوران ارسال می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>رد به بازبین فایل ← خسارت به WAITING_FOR_DAMAGE_EXPERT برمی‌گردد. محدودیت: حداکثر ۲ رد در هر خسارت؛ تلاش سوم ۴۲۲ با کد <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code> برمی‌گرداند.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-reviewer">۶ — بازبین فایل <span class="role-enum">file_reviewer</span></h2>
<p class="section-intro">
دومین اکتور در تقسیم V4/V5. بازبین فایل فایل‌های مهرومومه‌شده (پس از اتمام کار
فایل‌ساز) را تحویل می‌گیرد و مرحله کامل ارزیابی خسارت را انجام می‌دهد: فیلدهای
تصادف، دریافت الزامات عکس‌برداری، بارگذاری اسناد (شاسی/موتور)، انتخاب قطعات،
عکس‌های قطعات و ویدیوی دور زدن خودرو. تقصیر با car-capture به
COMPLETED علامت‌گذاری می‌شود. بازبین فایل همچنین دسترسی خواندن به پنل expert-claim
برای خسارت‌هایی که بررسی می‌کند دارد.
</p>
<div class="card card-teal">
<h3>ارزیابی خسارت — <code>v4/file-reviewer/blame-request-management/</code> و <code>v5/…</code></h3>
<p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست فایل‌های مهروموم‌شدهٔ فایل‌ساز که در شرکت بیمهٔ این بازبین برای دریافت آماده‌اند، به‌علاوهٔ فایل‌های تخصیص‌یافته به خود او.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل آماده برای دریافت یا تخصیص‌یافته، در شرکت بیمهٔ همین بازبین (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار (از استعلام طرف مقصر فایل‌ساز).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>مرحله ۱ (بازبین): ذخیره فیلدهای تصادف (accidentWay، accidentReason، accidentType).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فاز اسناد پیش از عکس‌برداری در مقابل فاز عکس‌برداری قطعات).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری اسناد شاسی / موتور / پلاک فلزی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>انتخاب قطعات آسیب‌دیده بیرونی (بدنه).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده (غیر بدنه).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا برای هر قطعه آسیب‌دیده انتخاب‌شده.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (آخرین مرحله عکس‌برداری بازبین). خسارت ← <code>WAITING_FOR_DAMAGE_EXPERT</code>، تقصیر ← <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>فقط برای خسارت‌های ترکیبیِ قیمت/فاکتور: ثبت موافقت با خطوط قیمت‌گذاری‌شده پیش از بارگذاری فاکتور. امضای نهایی مالک دیگر لازم نیست.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">در V4/V5 بی‌اثر است — تقصیر از قبل توسط car-capture COMPLETED شده. موفقیت idempotent برمی‌گرداند.</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>دسترسی به پنل expert-claim</h3>
<p class="note">
FILE_REVIEWER در نقش‌های مجاز برای <code>v2/expert-claim/</code> است.
می‌تواند جزئیات خسارت را مشاهده کند و جریان assign/lock را برای خسارت‌های
مرتبط با فایل‌هایش اجرا کند. نمی‌تواند به‌طور مستقل درخواست ارسال مجدد
کارشناس خسارت را آغاز کند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="registrar">۷ — ثبات <span class="role-enum">registrar</span></h2>
<p class="section-intro">
نقش اداری که تقصیر و خسارت حضوری را به نمایندگی از طرفین ثبت می‌کند.
از جریان OTP دسته‌ای استفاده می‌کند (OTPهای هر دو طرف به‌صورت همزمان ارسال و
تأیید می‌شوند) به جای OTP یک‌به‌یک که توسط کارشناسان میدانی استفاده می‌شود.
پس از تقصیر، ثبات آینه API خسارت کاربر را دنبال می‌کند تا انتخاب قطعات، اسناد
و عکس‌برداری را پر کند. سپس فایل وارد چرخه عادی بررسی کارشناس خسارت می‌شود.
</p>
<div class="card card-gray">
<h3>ثبت تقصیر — <code>registrar-initiated-blame/</code></h3>
<p class="note">توجه: <code>@ApiExcludeController</code> — مسیرها وجود دارند اما در مستندات Swagger نمایش داده نمی‌شوند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/create</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/my-files</code></td><td>فهرست تمام فایل‌های تقصیر ایجادشده توسط این ثبات.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/blame/:requestId</code></td><td>جزئیات کامل یک فایل تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/send-party-otps/:id</code></td><td>ارسال OTP به هر دو طرف به‌صورت همزمان.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/verify-party-otps/:id</code></td><td>تأیید OTPهای هر دو طرف در یک فراخوانی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/complete-blame-data/:id</code></td><td>ارسال تمام داده‌های فرم تقصیر هر دو طرف در یک پیلود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-video/:id</code></td><td>بارگذاری ویدیوی تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-voice/:id</code></td><td>بارگذاری ضبط صوتی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/add-accident-fields/:id</code></td><td>ذخیره فیلدهای تصادف و تکمیل تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-party-signature/:id</code></td><td>بارگذاری امضای یک طرف (partyRole=FIRST/SECOND).</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>ثبت خسارت — <code>v2/registrar/claim-request-management/</code></h3>
<p class="note">آینه‌ای از API خسارت کاربر. فرانت‌اند همان صفحات خسارت را با تغییر فقط پیشوند بازاستفاده می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create-from-blame/:blameId</code></td><td>ایجاد خسارت از یک فایل تقصیر تکمیل‌شده.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>outer-parts-catalog</code> / <code>car-other-part</code></td><td>کاتالوگ قطعات (قطعات بیرونی بدنه + JSON سایر قطعات).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>branches/:insuranceId</code></td><td>فهرست شعب بیمه‌گر (برای انتخاب شعبه در مرحله امضای خسارت).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>انتخاب قطعات آسیب‌دیده بیرونی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده + اطلاعات بانکی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری اسناد خسارت (گواهینامه، کارت خودرو).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (مرحله نهایی) ← WAITING_FOR_DAMAGE_EXPERT.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="call-center">۸ — مرکز تماس <span class="role-enum">call_center</span></h2>
<p class="section-intro">
ثبت تقصیر تلفنی V6 را مدیریت می‌کند. اپراتور داده‌های طرف مقصر را از طریق تلفن
جمع‌آوری می‌کند، استعلام بیمه را اجرا می‌کند و لینک تقصیر را از طریق SMS ارسال
می‌کند. کاربر سپس بقیه فرم را از طریق جریان استاندارد V2 تکمیل می‌کند
(با رد شدن مرحله فرم اولیه/استعلام). کار اپراتور مرکز تماس پس از send-link
پایان می‌یابد؛ می‌تواند پیشرفت را از طریق اندپوینت‌های خواندن پایش کند.
</p>
<div class="card card-indigo">
<h3>اندپوینت‌ها — <code>v6/call-center-blame/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create</code></td><td>ایجاد فایل تقصیر LINK. بدنه: <code>{ type: "THIRD_PARTY" | "CAR_BODY" }</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry/:requestId</code></td><td>اجرای استعلام بیمه پلاک + کد ملی برای طرف مقصر. نتیجه را روی سند تقصیر ذخیره می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry-vin/:requestId</code></td><td>VIN/شاسی جایگزین برای run-inquiry. از جستجوی شاسی ESG استفاده می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-link/:requestId</code></td><td>در صورت لزوم کاربر را ثبت‌نام می‌کند، به‌عنوان طرف اول ذخیره می‌کند، لینک دعوت تقصیر را از طریق SMS ارسال می‌کند. بدنه: <code>{ phoneNumber }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تقصیر شروع‌شده توسط این اپراتور.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>blame/:requestId</code></td><td>وضعیت فعلی و مرحله گردش کار یک فایل (برای بررسی اینکه کاربر لینک را باز کرده و پیشرفت کرده است).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
پس از <code>send-link</code>، کاربر فرم را از طریق
<code>v2/blame-request-management/</code> (جریان استاندارد V2) تکمیل می‌کند.
مرحله فرم اولیه / استعلام به‌صورت خودکار رد می‌شود
(<code>skipInitialFormStep=true</code>). جریان خسارت پایین‌دستی همان
جریان استاندارد خسارت V2 است.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2>مشترک: احراز هویت اکتورها</h2>
<p class="section-intro">
تمام اکتورهای پنل (هر نقش به جز <code>user</code>) از طریق همان اندپوینت
<code>POST actor/login</code> با کپچا احراز هویت می‌کنند. بازنشانی رمز عبور از
طریق OTP ایمیل است. خواندن و ویرایش پروفایل نیز مشترک است.
</p>
<div class="card card-gray">
<h3>اندپوینت‌ها — <code>actor/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/captcha</code></td><td>صدور یک چالش کپچای ورود جدید (captchaId + تصویر SVG را برمی‌گرداند).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/login</code></td><td>احراز هویت هر نقش اکتور. بدنه: role، username/email/nationalCode، password، captchaId، captcha. توکن‌های JWT دسترسی + رفرش را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password</code></td><td>ارسال OTP بازنشانی رمز عبور به ایمیل.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password-verify</code></td><td>تأیید OTP و تنظیم رمز عبور جدید.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/profile</code></td><td>دریافت پروفایل اکتور فعلی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>actor/profile</code></td><td>به‌روزرسانی پروفایل اکتور فعلی.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,617 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Panel Roles Reference</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, "Segoe UI", system-ui, sans-serif;
font-size: 14px;
line-height: 1.6;
background: #ffffff;
color: #1f2328;
padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.5;
}
/* Role header strip */
.role-header {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 6px;
}
.role-name {
font-size: 15px;
font-weight: 700;
}
.role-enum {
font-family: monospace;
font-size: 11px;
color: #3b82d4;
background: #f0f7ff;
border: 1px solid #bfdbfe;
border-radius: 4px;
padding: 1px 6px;
}
.badge {
display: inline-block;
font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
margin-right: 4px; margin-bottom: 3px;
}
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #dcfce7; color: #166534; }
.badge-purple { background: #ede9fe; color: #5b21b6; }
.badge-orange { background: #ffedd5; color: #9a3412; }
.badge-teal { background: #ccfbf1; color: #0f766e; }
.badge-indigo { background: #e0e7ff; color: #3730a3; }
.badge-gray { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.badge-red { background: #fee2e2; color: #991b1b; }
/* Cards */
.card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
background: #f7f8fa;
margin-bottom: 16px;
}
.card.card-blue { border-left: 4px solid #3b82f6; }
.card.card-green { border-left: 4px solid #22c55e; }
.card.card-purple { border-left: 4px solid #8b5cf6; }
.card.card-orange { border-left: 4px solid #f97316; }
.card.card-teal { border-left: 4px solid #14b8a6; }
.card.card-indigo { border-left: 4px solid #6366f1; }
.card.card-gray { border-left: 4px solid #94a3b8; }
/* Endpoint tables */
table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
margin-top: 4px;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: left; padding: 5px 8px;
border: 1px solid #e5e7eb;
}
td {
padding: 4px 8px; border: 1px solid #e5e7eb;
vertical-align: top;
}
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.method.delete { color: #dc2626; }
.dep { color: #94a3b8; font-style: italic; font-size: 11px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }
.note {
font-size: 11px; color: #57606a; font-style: italic;
margin-top: 6px;
}
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px;
margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-left: 18px; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb;
margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
/* Role overview table */
.overview-table { font-size: 12px; margin-bottom: 24px; }
.overview-table th { white-space: nowrap; }
.overview-table td:first-child { font-weight: 600; white-space: nowrap; }
</style>
</head>
<body>
<div class="max-wrap">
<h1>Panel Roles Reference</h1>
<p class="subtitle">
What every actor role can see and do — endpoints, responsibilities, and
process steps. Super-admin excluded.
</p>
<!-- Table of Contents -->
<div class="toc">
<div class="toc-title">Roles covered</div>
<ol>
<li><a href="#insurer">Insurer (COMPANY) — the insurance-company tenant admin</a></li>
<li><a href="#blame-expert">Blame Expert (EXPERT) — disagreement review queue</a></li>
<li><a href="#damage-expert">Damage Expert (DAMAGE_EXPERT) — claim pricing</a></li>
<li><a href="#field-expert">Field Expert (FIELD_EXPERT) — on-scene in-person filing</a></li>
<li><a href="#file-maker">File Maker (FILE_MAKER) — V4/V5 party narrative</a></li>
<li><a href="#file-reviewer">File Reviewer (FILE_REVIEWER) — V4/V5 damage assessment</a></li>
<li><a href="#registrar">Registrar (REGISTRAR) — office-based filing</a></li>
<li><a href="#call-center">Call Center (CALL_CENTER) — V6 phone-initiated filing</a></li>
</ol>
</div>
<!-- ── Role overview table ────────────────────────────────────── -->
<h2>Role Overview</h2>
<table class="overview-table">
<tr>
<th>Role enum</th>
<th>Login panel</th>
<th>Scope</th>
<th>Primary job</th>
</tr>
<tr>
<td><code>company</code></td>
<td>Insurer portal</td>
<td>Tenant-wide</td>
<td>View all files; manage branches, experts; run reports; rate experts</td>
</tr>
<tr>
<td><code>expert</code></td>
<td>Blame expert panel</td>
<td>Tenant DISAGREEMENT queue</td>
<td>Lock blame cases, review party submissions, submit verdict or request resend</td>
</tr>
<tr>
<td><code>damage_expert</code></td>
<td>Claim/damage panel</td>
<td>Tenant claim queue</td>
<td>Lock claims, price damage, validate repair factors, request resend/visit</td>
</tr>
<tr>
<td><code>field_expert</code></td>
<td>Field expert panel</td>
<td>Own created files</td>
<td>V2/V3 in-person blame + claim filing; also sees blame/claim review panels</td>
</tr>
<tr>
<td><code>file_maker</code></td>
<td>FileMaker panel</td>
<td>Own created files</td>
<td>V4/V5 party narrative (OTPs, inquiries, details, signatures); V5 claim approval</td>
</tr>
<tr>
<td><code>file_reviewer</code></td>
<td>FileReviewer panel</td>
<td>Assigned files</td>
<td>V4/V5 damage assessment (accident fields, parts, captures; mixed-factor priced-line acceptance when needed)</td>
</tr>
<tr>
<td><code>registrar</code></td>
<td>Registrar panel</td>
<td>Own created files</td>
<td>Office-based in-person blame + claim filing on behalf of parties</td>
</tr>
<tr>
<td><code>call_center</code></td>
<td>Call-center panel</td>
<td>Own created files</td>
<td>V6 phone-initiated blame: run inquiry, send link; user completes the rest</td>
</tr>
</table>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="insurer">1 — Insurer <span class="role-enum">company</span></h2>
<p class="section-intro">
One <code>company</code> actor per insurance-company tenant. The insurer portal is the
management layer: it can see everything under its tenant, manage the expert roster,
manage branches, configure per-tenant media settings, and pull statistical reports.
The insurer never touches blame/claim steps directly — it only observes and rates.
</p>
<div class="card card-blue">
<h3>File management — <code>expert-insurer/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files</code></td><td>List all blame + claim files for the tenant (merged by publicId). Filterable by status, file type, search, sort, page.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId</code></td><td>Full detail for one file by publicId.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/timeline</code></td><td>Chronological activity timeline (all history events: source, type, actor, metadata).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/report</code></td><td>Structured report data for PDF generation (owner, driver, insurance, vehicle, accident sections).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/files/:publicId/rating</code></td><td>Rate the experts on a file (1–5 per dimension: collision method, timeliness, cause accuracy, guilty-ID accuracy, bot rating).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/unified-file-statuses</code></td><td>Unified status catalog + per-status counts for the whole tenant portfolio. Filterable by fileType and date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Branch management — <code>expert-insurer/branches</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/branches</code></td><td>List all branches for this insurer. Query: search, from/to date, isActive filter.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/branches</code></td><td>Add a new branch (name, code, address, city, phone, etc.).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/branches/:branchId/status</code></td><td>Activate or deactivate a branch.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Expert roster management — <code>expert-insurer/experts</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/blame</code></td><td>Create a new blame-expert account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/claim</code></td><td>Create a new damage-expert (claim) account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-maker</code></td><td>Create a new FileMaker account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-reviewer</code></td><td>Create a new FileReviewer account under this insurer.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/list</code></td><td>Paginated list of all expert accounts on this tenant.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/top</code></td><td>Top blame vs claim experts ranked by overall average rating (up to 10 each).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-experts</code></td><td>Alias for experts/top (frontend compat).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/:expertId</code></td><td>Files handled by one expert (slim summary rows — blame or claim depending on expert type).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Statistics &amp; reports</h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/statistics</code></td><td>KPI cards: totalFilesReviewed, averageUserRating, inPersonCount, filesThisMonth, objectionPercentage, etc. Filterable by date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-files</code></td><td>Top 10 highest-rated claim files (combined insurer + user score).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/expert-work-log</code></td><td>Per-expert work log: totalHandled, currentlyChecking, distinctFilesCheckedInPeriod. Filterable by expertKind and date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/requests</code></td><td>Claim + blame status bucket counts + unified file count for the tenant.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/per-month-requests</code></td><td>Same summary, broken down by the last 5 calendar months.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/checked-requests</code></td><td>Same summary filtered by optional createdAt date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log</code></td><td>Expert work log (blame + damage expert collections, not field experts).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log/per-month</code></td><td>Same work log per calendar month (last 5).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Tenant settings — <code>client-panel/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>client-panel/settings</code></td><td>Get per-tenant media limits (video/image/voice maxBytes) and CAR_BODY accident window (days).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>client-panel/settings</code></td><td>Update those settings (partial). Cannot exceed system-level route ceilings.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="blame-expert">2 — Blame Expert <span class="role-enum">expert</span></h2>
<p class="section-intro">
Reviews blame files in the DISAGREEMENT queue — cases where the two parties do not
agree on who is at fault. After reviewing submitted documents and party statements
the expert locks the case, then either submits a verdict, asks the parties to resend
documents, or records an in-person visit outcome. All endpoints are under
<code>v2/expert-blame/</code>.
</p>
<div class="card card-orange">
<h3>Process</h3>
<p class="note">
1 Browse list → 2 Assign (lock) the case → 3 Review party evidence (videos, voices, documents) →
4a Submit verdict <em>or</em> 4b Request document resend <em>or</em> 4c Record in-person visit.
</p>
</div>
<div class="card card-orange">
<h3>Endpoints — <code>v2/expert-blame/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/</code></td><td>List blame cases in the DISAGREEMENT queue (available, locked by me, or decided by me). Query: search, sortBy, sortOrder, page, limit, unifiedStatus, fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/:id</code></td><td>Full detail for one blame case (party statements, photos, voices, videos).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-blame/:id/assign</code></td><td>Check availability and lock the case to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409 if someone else holds it.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/submit/:id</code></td><td>Submit verdict (accidentWay, accidentReason, accidentType, guilty party decision). Unlocks the case and moves it to COMPLETED.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/resend/:id</code></td><td>Request parties to re-upload documents. Sets blame to WAITING_FOR_RESEND. One resend request per lifecycle.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/inPerson/:id</code></td><td>Record that an in-person visit was made and submit verdict.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/unified-file-statuses</code></td><td>Status catalog + per-status counts for this expert's portfolio.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/lock/:id</code></td><td class="dep">Deprecated lock endpoint — use POST assign.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="damage-expert">3 — Damage Expert <span class="role-enum">damage_expert</span></h2>
<p class="section-intro">
Reviews claim files after the user has submitted their damage evidence. The expert
prices each damaged part, optionally calculates a price-drop (depreciation), and
can ask the user to resend documents, come in person, or upload repair factor invoices
when workshop pricing is needed. All endpoints are under <code>v2/expert-claim/</code>.
</p>
<div class="card card-red">
<h3>Process</h3>
<p class="note">
1 Browse list → 2 Assign (lock) the claim → 3 Review damage photos and documents →
4 Optionally edit selected parts or calculate price-drop →
5a Submit priced reply <em>or</em> 5b Request resend <em>or</em> 5c Request in-person visit →
6 If factor parts present: validate uploaded factor invoices.
</p>
</div>
<div class="card card-red">
<h3>Endpoints — <code>v2/expert-claim/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/requests</code></td><td>List claims in <code>WAITING_FOR_DAMAGE_EXPERT</code> queue + factor-validation queue. Query: search, sortBy, page, limit, unifiedStatus, fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId</code></td><td>Full claim detail: damaged parts, captured images, documents, priceDrop, blameCase party data, video URLs.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>Lock claim to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Price-drop context: severity labels, coefficient catalog, damaged parts + mapping, suggested car year from blame inquiry.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Calculate and persist price-drop: carPrice × yearCoeff × sumOfCoeffs ÷ 400.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>Submit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. A priced-only claim completes immediately; factor claims continue through factor collection/validation. No final owner signature or automatic Fanavaran submission.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>Request user to resend documents/photos. One resend per claim lifecycle; returns 422 if already fulfilled.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>Ask user to come in person. Unlocks claim, sets claimStatus to NEEDS_REVISION.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>Validate uploaded repair factor invoices. Approve or reject each factor line with totalPayment. Cap applies across all lines (≤ 53 000 000 Toman). Auto-completes when all lines are decided.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/request/:claimRequestId/damaged-parts</code></td><td>Edit selected damaged parts while the claim is locked by this expert (EXPERT_REVIEWING).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/outer-parts-catalog</code></td><td>Fanavaran outer car-components catalog (shared with user flow).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/inner-parts-catalog</code></td><td>Static inner car-parts catalog JSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/branches</code></td><td>Insurer branches for this expert's tenant (for daghi/branch selection in reply payload).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/stream/:id/video</code></td><td>Stream claim video (car-capture walk-around or accident video). Query: <code>query=car-capture|accident</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/unified-file-statuses</code></td><td>Status catalog + counts for this expert's claim portfolio.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/lock/:claimRequestId</code></td><td class="dep">Deprecated lock endpoint — use POST assign.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="field-expert">4 — Field Expert <span class="role-enum">field_expert</span></h2>
<p class="section-intro">
Goes to the accident scene and fills both parties' forms in-person (V2 mirror / V3 flows).
The field expert also has read access to the expert-blame and expert-claim panels
(scoped to their own files). They are the only role that spans both
<strong>blame filing</strong> and <strong>claim filing</strong> in the same session.
</p>
<div class="card card-green">
<h3>Blame filing — <code>v2/expert-initiated/blame-request-management/</code></h3>
<p class="note">Mirror of the user blame API. Frontend reuses same pages by swapping prefix only.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code></td><td>Send OTP to one party by phone number (no invite link).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>verify-party-otp/:id</code></td><td>Verify one party's OTP and bind their account.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>blame-confession/:id</code></td><td>Record party's blame confession.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[CAR_BODY only] Accident type form.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Initial form / plate or VIN inquiry for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:id</code></td><td>Upload first-party video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code></td><td>Add GPS location for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-voice/:id</code></td><td>Upload voice recording for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-description/:id</code></td><td>Add description for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-second-party/:phone/:id/</code></td><td>Advance to second party (no SMS link sent).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (FIRST then SECOND, partyRole param).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>Save accident fields and complete blame immediately (no expert queue).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>V3 blame + claim filing — <code>v3/expert-initiated/blame-request-management/</code></h3>
<p class="note">Reorganised step order: all party narrative first, then damage assessment. Blame and claim both handled in this single controller.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code> → <code>send-party-otp</code> → <code>verify-party-otp</code> → <code>run-inquiries</code> → <code>add-detail-*</code> → <code>sign</code> (×2)</td><td>Party narrative phase (steps 1–8) — identical endpoints to mirror, same contract.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>Step 9: save accident fields after both parties have signed.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Step 10: get auto-created claim ID.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Step 11: upload licence / car card documents.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code> / <code>select-other-parts/:claimId</code></td><td>Steps 12–13: select damaged parts.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Step 14: capture part photos + angles.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Step 15: walk-around video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td>Step 16: blame accident video (final) → WAITING_FOR_EXPERT (THIRD_PARTY) or COMPLETED (CAR_BODY).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>Expert-blame + expert-claim panel (read + action on own files)</h3>
<p class="note">
FIELD_EXPERT sees <code>v2/expert-blame/</code> scoped to their own created files (not the disagreement queue).
They also see <code>v2/expert-claim/</code> for claims linked to their blame files.
Same endpoints as blame-expert and damage-expert panels above.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-maker">5 — File Maker <span class="role-enum">file_maker</span></h2>
<p class="section-intro">
The first actor in the V4/V5 split. FileMaker handles the party narrative on-site:
OTPs, inquiries, location/description/voice, and signatures for both parties.
They also upload the initial claim documents (licences, car cards). After the second
signature the file is "sealed" for FileReviewer pickup. In V5, FileMaker comes
back at the end to approve or reject the completed claim. After approval, an
expert submits the case to Fanavaran manually.
</p>
<div class="card card-purple">
<h3>Blame filing — <code>v4/file-maker/blame-request-management/</code> and <code>v5/…</code></h3>
<p class="note">V4 and V5 endpoints are identical — only the prefix changes. V5 sets <code>requiresFileMakerApproval=true</code> at creation.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all blame files created by this FileMaker.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one file (parties, workflow, linked claim ID).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID after guilty-party run-inquiries.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code> / <code>verify-party-otp/:id</code></td><td>Send + verify OTP for one party at a time (guilty first, then damaged).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[CAR_BODY only] Accident type form.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Run plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>Add location, description, and voice for current party (partyRole param selects FIRST/SECOND).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload licences / car cards against the auto-created claim.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine).</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>V5 claim approval — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">Used only in V5. After damage expert review and any required factor validation, claim enters <code>WAITING_FOR_FILE_MAKER_APPROVAL</code>; no final owner signature is needed.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>Approve the completed claim → claim becomes <code>COMPLETED</code>. An expert submits to Fanavaran manually when ready.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>Reject back to FileReviewer → claim returns to WAITING_FOR_DAMAGE_EXPERT. Limit: max 2 rejections per claim; 3rd attempt returns 422 <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code>.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-reviewer">6 — File Reviewer <span class="role-enum">file_reviewer</span></h2>
<p class="section-intro">
The second actor in the V4/V5 split. FileReviewer picks up sealed files (after
FileMaker is done) and performs the full damage assessment pass: accident fields,
capture requirements lookup, document upload (chassis/engine), part selection,
part photos and walk-around video. The blame is marked COMPLETED
by car-capture. FileReviewer also has read access to the expert-claim panel for
claims they are reviewing.
</p>
<div class="card card-teal">
<h3>Damage assessment — <code>v4/file-reviewer/blame-request-management/</code> and <code>v5/…</code></h3>
<p class="note">V4 and V5 endpoints are identical — only the prefix changes.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List FileMaker-sealed files available to claim in this reviewer’s insurer, plus files already assigned to this FileReviewer.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one available or assigned file in this reviewer’s insurer (parties, workflow, expert fields, linked claim ID).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID (from FileMaker's guilty-party inquiry).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>Step 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (pre-capture docs phase vs capture-parts phase).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload chassis / engine / metal-plate documents.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer (body) damaged parts.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other (non-body) damaged parts.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles for each selected damaged part.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final FileReviewer capture step). Claim → <code>WAITING_FOR_DAMAGE_EXPERT</code>, blame → <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>For mixed priced/factor claims only: record acceptance of priced lines before factor uploads. A final owner signature is no longer required.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">No-op in V4/V5 — blame already COMPLETED by car-capture. Returns idempotent success.</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>Expert-claim panel access</h3>
<p class="note">
FILE_REVIEWER is in the allowed roles for <code>v2/expert-claim/</code>.
They can view claim details and run the assign/lock flow for claims associated
with their files. They cannot initiate a damage-expert resend independently.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="registrar">7 — Registrar <span class="role-enum">registrar</span></h2>
<p class="section-intro">
Office-based role that files in-person blame and claim on behalf of parties.
Uses a bulk-OTP flow (both parties' OTPs sent and verified in one call each)
rather than the one-at-a-time OTP used by field experts. After blame, the
registrar mirrors the user claim API to fill part selection, documents, and
captures. The file then enters the normal damage-expert review lifecycle.
</p>
<div class="card card-gray">
<h3>Blame filing — <code>registrar-initiated-blame/</code></h3>
<p class="note">Note: <code>@ApiExcludeController</code> — routes exist but not surfaced in Swagger docs.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/create</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/my-files</code></td><td>List all blame files created by this registrar.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/blame/:requestId</code></td><td>Full detail for one blame file.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/send-party-otps/:id</code></td><td>Send OTPs to both parties simultaneously.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/verify-party-otps/:id</code></td><td>Verify both parties' OTPs in one call.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/complete-blame-data/:id</code></td><td>Submit all blame form data for both parties in one payload.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-video/:id</code></td><td>Upload blame video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-voice/:id</code></td><td>Upload voice recording.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/add-accident-fields/:id</code></td><td>Save accident fields and complete blame.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-party-signature/:id</code></td><td>Upload a party's signature (partyRole=FIRST/SECOND).</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>Claim filing — <code>v2/registrar/claim-request-management/</code></h3>
<p class="note">Mirror of the user claim API. Frontend reuses same claim pages by swapping prefix only.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create-from-blame/:blameId</code></td><td>Create claim from a completed blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>outer-parts-catalog</code> / <code>car-other-part</code></td><td>Parts catalogs (outer body parts + other parts JSON).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>branches/:insuranceId</code></td><td>Insurer branch list (for branch selection in claim sign step).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer damaged parts.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other damaged parts + bank info.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload claim documents (licences, car card).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final step) → WAITING_FOR_DAMAGE_EXPERT.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="call-center">8 — Call Center <span class="role-enum">call_center</span></h2>
<p class="section-intro">
Handles V6 phone-initiated blame filing. The agent collects the guilty party's
data over the phone, runs the insurance inquiry, and sends the blame link via
SMS. The user then completes the rest of the form through the standard V2 flow
(with the initial-form/inquiry step skipped). The call-center agent's job ends
after send-link; they can monitor progress via the read endpoints.
</p>
<div class="card card-indigo">
<h3>Endpoints — <code>v6/call-center-blame/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create</code></td><td>Create a LINK blame file. Body: <code>{ type: "THIRD_PARTY" | "CAR_BODY" }</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry/:requestId</code></td><td>Run plate + national-code insurance inquiry for the guilty party. Stores result on blame document.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry-vin/:requestId</code></td><td>VIN/chassis alternative to run-inquiry. Uses ESG chassis lookup.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-link/:requestId</code></td><td>Register user if needed, store as first party, send blame invite link via SMS. Body: <code>{ phoneNumber }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all blame files started by this agent.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>blame/:requestId</code></td><td>Current status and workflow step for one file (to check if user has opened the link and progressed).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
After <code>send-link</code> the user completes the form via
<code>v2/blame-request-management/</code> (standard V2 flow).
The initial-form / inquiry step is automatically skipped
(<code>skipInitialFormStep=true</code>). Downstream claim flow is the
standard V2 claim flow.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2>Shared: Actor Authentication</h2>
<p class="section-intro">
All panel actors (every role except <code>user</code>) authenticate through the same
<code>POST actor/login</code> endpoint with captcha. Password reset is via email OTP.
Profile reads and edits are also shared.
</p>
<div class="card card-gray">
<h3>Endpoints — <code>actor/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/captcha</code></td><td>Issue a new login captcha challenge (returns captchaId + SVG image).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/login</code></td><td>Authenticate any actor role. Body: role, username/email/nationalCode, password, captchaId, captcha. Returns JWT access + refresh tokens.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password</code></td><td>Send password-reset OTP to email.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password-verify</code></td><td>Verify OTP and set new password.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/profile</code></td><td>Get current actor's profile.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>actor/profile</code></td><td>Update current actor's profile.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -1 +0,0 @@
{"1000":{"info":"start","message":"start"},"1001":{"info":"Access Denied Other Actor Lock File","message":""},"1004":{"info":"g","message":""},"1005":{"info":"fSF","message":""},"1006":{"info":"request not found ","message":""}}

View File

@@ -3,6 +3,13 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": true,
"assets": [
{
"include": "../assets/fonts/**/*",
"outDir": "dist",
"watchAssets": true
}
]
}
}

18169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yara724",
"version": "0.0.1",
"version": "2.0.0",
"description": "",
"author": "",
"private": true,
@@ -12,82 +12,71 @@
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"seed:parsian-tehran": "ts-node scripts/seed-parsian-tehran.ts",
"seed:reports-fixtures": "ts-node scripts/seed-insurer-reports-fixtures.ts",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"engines": {
"npm": ">=10.0.0",
"node": ">=20.0.0"
},
"dependencies": {
"@arashioz/errjson-talieh": "^2.2.5",
"@fraybabak/kavenegar_nest": "^1.0.5",
"@nestjs-modules/mailer": "^1.8.1",
"@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/mongoose": "^10.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/platform-fastify": "^10.4.15",
"@nestjs/platform-socket.io": "^10.4.15",
"@nestjs/schedule": "^4.1.2",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/swagger": "^7.4.2",
"@nestjs/websockets": "^10.4.15",
"@types/uuid": "^10.0.0",
"axios": "^1.9.0",
"bcrypt": "^5.1.1",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.17",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/mongoose": "^11.0.4",
"@nestjs/platform-express": "^11.1.11",
"@nestjs/serve-static": "^5.0.5",
"@nestjs/swagger": "^11.4.4",
"axios": "^1.16.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto": "^1.0.1",
"dotenv": "^16.4.7",
"express-basic-auth": "^1.2.1",
"class-validator": "^0.15.1",
"express": "^5.2.1",
"fastest-levenshtein": "^1.0.16",
"form-data": "^4.0.2",
"jalali-moment": "^3.3.11",
"kavenegar": "^1.1.4",
"form-data": "^4.0.6",
"joi": "^18.2.1",
"mongoose": "^8.9.2",
"nestjs-command": "^3.1.4",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pdfkit": "^0.19.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"short-unique-id": "^5.2.0",
"standard": "^17.1.2",
"standardjs": "^1.0.0-alpha",
"uuid": "^11.0.3",
"yargs": "^17.7.2"
"socks-proxy-agent": "^8.0.4",
"svg-captcha": "^1.4.0"
},
"devDependencies": {
"@nestjs/cli": "^11.0.14",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
"@compodoc/compodoc": "^2.0.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.10.8",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.2",
"@types/passport-jwt": "^4.0.1",
"@types/multer": "^2.1.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"ts-standard": "^12.0.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.2"
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
@@ -100,10 +89,19 @@
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"pnpm": {
"onlyBuiltDependencies": [
"@nestjs/core",
"@swc/core"
]
}
}

View File

@@ -0,0 +1,91 @@
# Fanavaran flow test — copy this file, fill section A, then run:
#
# cp scripts/data/fanavaran-flow.env.example scripts/data/fanavaran-flow.moallem.env
# ./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env
#
# Section A = you fill before (or when the script asks).
# Section B = leave empty. The script writes Fanavaran ids here after each stage
# so you can stop, re-run, or continue without copying ids by hand.
#
# Do not commit real secrets or national codes.
# =============================================================================
# A) FILL BEFORE RUN
# =============================================================================
# --- tenant ---
FANAVARAN_CLIENT=moallem
# Localhost only: route curl through Termius SOCKS (Dynamic Port Forwarding).
# Example: CURL_PROXY=socks5h://127.0.0.1:1080
# Or run the script on the Moallem server (no proxy needed).
# CURL_PROXY=
# Optional auth overrides (omit to use built-in seeds for this client)
# APP_NAME=ItTalie
# APP_SECRET=
# FANAVARAN_USERNAME=
# FANAVARAN_PASSWORD=
# CORP_ID=
# CONTRACT_ID=
# LOCATION=
# --- tenant lookup ids (from this insurer's Fanavaran lookups) ---
CLAIM_EXPERT_ID=
EXPERTISE_CLAIM_EXPERT_ID=
CLAIM_FILE_TYPE_ID=
VEHICLE_KIND_ID=
DMG_SECTION_ID=
# Persian caption OR numeric Fanavaran Id
INSURANCE_CORP_ID=
# --- GEN.03 case ---
GUILTY_NATIONAL_CODE=
ACCIDENT_DATE=1404/05/20
ACCIDENT_TIME=12:00
# --- GEN.12 case ---
DRIVER_NATIONAL_CODE=
DRIVER_BIRTH_YEAR=1370
DRIVER_BIRTH_MONTH=1
DRIVER_BIRTH_DAY=1
DRIVER_IS_INSURER=0
LICENCE_NO=
DESC=سپر عقب
# Optional vehicle / plate / policy document (leave empty if unknown)
# PLAQUE_LEFT_NO=
# PLAQUE_RIGHT_NO=
# PLAQUE_SERIAL=
# PLAQUE_MIDDLE_CODE_ID=
# PLAQUE_NO=
# CHASSIS_NO=
# MOTOR_NO=
# VIN=
# POLICY_NO=
# POLICY_CI_NUMBER=
# BEGIN_DATE=
# END_DATE=
# BUILT_YEAR=
# --- GEN.07 ---
ATTACHMENT_FILE=
# --- GEN.08 ---
DMG_ASSESSMENT_DATE=1404/05/20
INSPECTION_TIME=12:00
REPAIR_WAGE=0
COMPONENT_REPLACEMENT_COST=0
WASTE_VALUE=0
# =============================================================================
# B) FILLED BY SCRIPT (do not set these before the first run)
# =============================================================================
POLICY_ID=
CLAIM_ID=
CLAIM_NO=
DRIVER_ID=
INSURANCE_CORP_ID_NUM=
DMG_CASE_ID=
EXPERTISE_ID=

View File

@@ -0,0 +1,65 @@
{
"clientCode": 8,
"branches": [
{
"code": "210120",
"name": "شعبه والفجر",
"fullName": "شعبه والفجر(210120)",
"city": "تهران",
"state": "تهران",
"address": "تهران، اميرآبادشمالي، شهرک والفجر، ضلع جنوب غربي ميدان استادخسرو سينايي",
"phoneNumber": "86051332",
"isActive": true
},
{
"code": "210050",
"name": "شعبه غرب تهران",
"fullName": "شعبه غرب تهران(210050)",
"city": "تهران",
"state": "تهران",
"address": "تهران ـخيابان آزادي ( محله تيموري )، نبش خيابان شهيد داود حبيب زادگان پلاک 2 - 1458887853",
"phoneNumber": "66021968",
"isActive": true
},
{
"code": "110011",
"name": "ستاد مرکزي",
"fullName": "ستاد مرکزي(110011)",
"city": "تهران",
"state": "تهران",
"address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22",
"phoneNumber": "8259",
"isActive": true
},
{
"code": "210040",
"name": "شعبه شرق تهران",
"fullName": "شعبه شرق تهران(210040)",
"city": "تهران",
"state": "تهران",
"address": "تهران، خيابان دماوند، بعداز چهارراه تهرانپارس، روبروي تعميرگاه مرکزي شماره يک سايپا، پلاک129",
"phoneNumber": "77393783-4",
"isActive": true
},
{
"code": "210110",
"name": "شعبه پونک",
"fullName": "شعبه پونک(210110)",
"city": "تهران",
"state": "تهران",
"address": "تهران، خيابان ميرزا بابايي، نبش خيابان سردارجنگل، پارك سوارپونك",
"phoneNumber": "44452270",
"isActive": true
},
{
"code": "111130",
"name": "شعبه ويژه ميرداماد",
"fullName": "شعبه ويژه ميرداماد(111130)",
"city": "تهران",
"state": "تهران",
"address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22",
"phoneNumber": "8259",
"isActive": true
}
]
}

View File

@@ -0,0 +1,153 @@
{
"clientCode": 8,
"fieldExperts": [
{
"nationalCode": "0013480261",
"firstName": "عليرضا",
"lastName": "خازني",
"branchCode": "210040",
"branchName": "شعبه شرق تهران(210040)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "0051967839",
"mobile": "09121354859",
"firstName": "حسين",
"lastName": "جعفري",
"branchCode": "210120",
"branchName": "شعبه والفجر",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "0056888082",
"mobile": "09122406750",
"firstName": "قاسم",
"lastName": "نصراللهي",
"branchCode": "210050",
"branchName": "شعبه غرب تهران(210050)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت ثالث مالي",
"expertCode": "4664"
},
{
"nationalCode": "0066868521",
"mobile": "09129344240",
"firstName": "عليرضا",
"lastName": "گودرزي پور",
"branchCode": "210050",
"branchName": "شعبه غرب تهران(210050)",
"title": "كارشناس ارزياب خسارت بدنه",
"expertCode": "4663"
},
{
"nationalCode": "0076988961",
"mobile": "09108357378",
"firstName": "مهدي",
"lastName": "روشن دل",
"branchCode": "210110",
"branchName": "شعبه پونک",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "0078209129",
"mobile": "09126038117",
"firstName": "مهدي",
"lastName": "شاملوفرد",
"branchCode": "210050",
"branchName": "شعبه غرب تهران(210050)",
"title": "كارشناس ارزياب خسارت ثالث مالي",
"expertCode": "4666"
},
{
"nationalCode": "0083730397",
"mobile": "09125759960",
"firstName": "مجيد",
"lastName": "کاظمي دولت سرا",
"branchCode": "210120",
"branchName": "شعبه والفجر",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "0084130938",
"mobile": "09392558640",
"firstName": "رسول",
"lastName": "کرکي",
"branchCode": "210050",
"branchName": "شعبه غرب تهران(210050)",
"title": "كارشناس ارزياب خسارت ثالث مالي",
"expertCode": "4662"
},
{
"nationalCode": "0440245151",
"mobile": "09130606183",
"firstName": "فرهاد",
"lastName": "ملکي مونقي",
"branchCode": "110011",
"branchName": "ستاد مرکزي",
"title": "كارشناس ارزياب خسارت ثالث مالي"
},
{
"nationalCode": "0493217789",
"mobile": "09126966943",
"firstName": "مصطفي",
"lastName": "محمدزاده قورقچي",
"branchCode": "210050",
"branchName": "شعبه غرب تهران(210050)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت ثالث مالي",
"expertCode": "4665"
},
{
"nationalCode": "0670358118",
"mobile": "09124421539",
"firstName": "مجيد",
"lastName": "اميري",
"branchCode": "210040",
"branchName": "شعبه شرق تهران(210040)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت ثالث مالي"
},
{
"nationalCode": "0759153981",
"firstName": "رضا",
"lastName": "صالحي زاده",
"branchCode": "210040",
"branchName": "شعبه شرق تهران(210040)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "1262982308",
"mobile": "09130121246",
"firstName": "روح الله",
"lastName": "سلمانيان مقدم نياسري",
"branchCode": "210120",
"branchName": "شعبه والفجر",
"city": "کاشان",
"state": "اصفهان",
"title": "كارشناس ارزياب خسارت بدنه"
},
{
"nationalCode": "3781847039",
"firstName": "اکبر",
"lastName": "ديني",
"branchCode": "210040",
"branchName": "شعبه شرق تهران(210040)",
"city": "تهران",
"state": "تهران",
"title": "كارشناس ارزياب خسارت ثالث مالي"
}
]
}

View File

@@ -0,0 +1,464 @@
{
"clientCode": 8,
"fileMakers": [
{
"nationalCode": "0061077410",
"mobile": "09125864643",
"firstName": "احمد",
"lastName": "بستان پيرا",
"locations": [
{
"id": "210110",
"name": "شعبه پونک"
}
],
"ThirdPartyClaimExpertId": "32",
"CarBodyClaimExpertId": "33"
},
{
"nationalCode": "0492009856",
"mobile": "09128465836",
"firstName": "اسداله",
"lastName": "نجفي پور",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "91",
"CarBodyClaimExpertId": "92"
},
{
"nationalCode": "0062277553",
"mobile": "09125012274",
"firstName": "اسماعيل",
"lastName": "سلطانمحمدي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4221",
"CarBodyClaimExpertId": "4222"
},
{
"nationalCode": "0492295557",
"mobile": "09125409101",
"firstName": "اکبر",
"lastName": "بيگ محمدي",
"locations": [
{
"id": "111130",
"name": "شعبه ويژه ميرداماد"
},
{
"id": "110011",
"name": "ستاد مرکزي"
}
],
"ThirdPartyClaimExpertId": "278"
},
{
"nationalCode": "3220096573",
"mobile": "09104933206",
"firstName": "پدرام",
"lastName": "حاتمي",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyClaimExpertId": "219"
},
{
"nationalCode": "0078954010",
"mobile": "09128894315",
"firstName": "پروانه",
"lastName": "آدابي",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "739",
"CarBodyClaimExpertId": "740"
},
{
"nationalCode": "0075127105",
"mobile": "09122938429",
"firstName": "حميد",
"lastName": "غوثي هوجقان",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"CarBodyClaimExpertId": "3215",
"ThirdPartyClaimExpertId": "3214"
},
{
"nationalCode": "0081114494",
"mobile": "09374439044",
"firstName": "داود",
"lastName": "صديق",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyClaimExpertId": "222",
"CarBodyClaimExpertId": "223"
},
{
"nationalCode": "0084130938",
"mobile": "09392558640",
"firstName": "رسول",
"lastName": "کرکي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4662"
},
{
"nationalCode": "0014103788",
"mobile": "09118861247",
"firstName": "سجاد",
"lastName": "حشمت",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "4245",
"CarBodyClaimExpertId": "4246"
},
{
"nationalCode": "0065938100",
"mobile": "09122366860",
"firstName": "سهيلا",
"lastName": "شکوري قره چيق",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"CarBodyClaimExpertId": "89",
"ThirdPartyClaimExpertId": "88"
},
{
"nationalCode": "5779888949",
"mobile": "09371290042",
"firstName": "عاطفه",
"lastName": "نصيري",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"CarBodyClaimExpertId": "4455",
"ThirdPartyClaimExpertId": "4454"
},
{
"nationalCode": "0069608210",
"mobile": "09120766792",
"firstName": "عباس",
"lastName": "سلطاني محمدي",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "742",
"CarBodyClaimExpertId": "743"
},
{
"nationalCode": "0065409027",
"mobile": "09125045732",
"firstName": "علي",
"lastName": "اصلاني حاجي آبادي",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyClaimExpertId": "4338",
"CarBodyClaimExpertId": "4339"
},
{
"nationalCode": "4570007309",
"mobile": "09104873190",
"firstName": "علي",
"lastName": "قرباني",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"CarBodyClaimExpertId": "4540",
"ThirdPartyClaimExpertId": "4539"
},
{
"nationalCode": "0077396881",
"mobile": "09120000001",
"firstName": "علي",
"lastName": "مهرجو",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"CarBodyClaimExpertId": "4652",
"ThirdPartyClaimExpertId": "4650"
},
{
"nationalCode": "0068289782",
"mobile": "09124196488",
"firstName": "عليرضا",
"lastName": "درگاهي",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyClaimExpertId": "57",
"CarBodyClaimExpertId": "58"
},
{
"nationalCode": "0066868521",
"mobile": "09129344240",
"firstName": "عليرضا",
"lastName": "گودرزي پور",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4663"
},
{
"nationalCode": "0061947466",
"mobile": "09125045283",
"firstName": "غزال",
"lastName": "عطائي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"CarBodyClaimExpertId": "4220",
"ThirdPartyClaimExpertId": "4219"
},
{
"nationalCode": "0062500767",
"mobile": "09124464022",
"firstName": "غلامرضا",
"lastName": "حسن پوراقدم",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"CarBodyClaimExpertId": "4452",
"ThirdPartyClaimExpertId": "4451"
},
{
"nationalCode": "0019675526",
"mobile": "09120399833",
"firstName": "فاطمه",
"lastName": "عظيميان",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"CarBodyClaimExpertId": "4340",
"ThirdPartyClaimExpertId": "4341"
},
{
"nationalCode": "0056888082",
"mobile": "09122406750",
"firstName": "قاسم",
"lastName": "نصراللهي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4664"
},
{
"nationalCode": "3932143930",
"mobile": "09125255267",
"firstName": "محسن",
"lastName": "کرمي",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"CarBodyClaimExpertId": "4415",
"ThirdPartyClaimExpertId": "4414"
},
{
"nationalCode": "0013269755",
"mobile": "09195502061",
"firstName": "محمد",
"lastName": "ابراهيمي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"CarBodyClaimExpertId": "4092",
"ThirdPartyClaimExpertId": "4091"
},
{
"nationalCode": "0011834803",
"mobile": "09127059125",
"firstName": "محمد",
"lastName": "ترابي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "1010",
"CarBodyClaimExpertId": "1011"
},
{
"nationalCode": "0493217789",
"mobile": "09126966943",
"firstName": "مصطفي",
"lastName": "محمدزاده قورقچي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4665"
},
{
"nationalCode": "0080501281",
"mobile": "09356468730",
"firstName": "موسي",
"lastName": "موسي نژاد",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"CarBodyClaimExpertId": "745",
"ThirdPartyClaimExpertId": "744"
},
{
"nationalCode": "0078209129",
"mobile": "09126038117",
"firstName": "مهدي",
"lastName": "شاملوفرد",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "4666"
},
{
"nationalCode": "0074009141",
"mobile": "09339103762",
"firstName": "مهدي",
"lastName": "معمارباشي",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyClaimExpertId": "216",
"CarBodyClaimExpertId": "217"
},
{
"nationalCode": "3720211207",
"mobile": "09379120944",
"firstName": "مهسا",
"lastName": "وطن نيا",
"locations": [
{
"id": "210110",
"name": "شعبه پونک"
}
],
"ThirdPartyClaimExpertId": "34",
"CarBodyClaimExpertId": "35"
},
{
"nationalCode": "0079616801",
"mobile": "09379669839",
"firstName": "مهيار",
"lastName": "کيائي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyClaimExpertId": "1004",
"CarBodyClaimExpertId": "1005"
},
{
"nationalCode": "0079815278",
"mobile": "09195883178",
"firstName": "ميگل",
"lastName": "ميرشکاري",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "4646",
"CarBodyClaimExpertId": "4648"
},
{
"nationalCode": "0010736638",
"mobile": "09355242492",
"firstName": "ناصر",
"lastName": "عيوضي",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyClaimExpertId": "228",
"CarBodyClaimExpertId": "229"
}
]
}

View File

@@ -0,0 +1,161 @@
{
"clientCode": 8,
"fileReviewers": [
{
"nationalCode": "0051967839",
"mobile": "09121354859",
"firstName": "حسين",
"lastName": "جعفري",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyExpertiseClaim": "3542",
"CarBodyExpertiseClaim": "3541"
},
{
"nationalCode": "0084130938",
"mobile": "09392558640",
"firstName": "رسول",
"lastName": "کرکي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyExpertiseClaim": "29",
"CarBodyExpertiseClaim": "28"
},
{
"nationalCode": "1262982308",
"mobile": "09130121246",
"firstName": "روح الله",
"lastName": "سلمانيان مقدم نياسري",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"CarBodyExpertiseClaim": "3705"
},
{
"nationalCode": "0066868521",
"mobile": "09129344240",
"firstName": "عليرضا",
"lastName": "گودرزي پور",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"CarBodyExpertiseClaim": "15",
"ThirdPartyExpertiseClaim": "16"
},
{
"nationalCode": "0440245151",
"mobile": "09130606183",
"firstName": "فرهاد",
"lastName": "ملکي مونقي",
"locations": [
{
"id": "110011",
"name": "ستاد مرکزي"
}
],
"ThirdPartyExpertiseClaim": "60",
"CarBodyExpertiseClaim": "61"
},
{
"nationalCode": "0056888082",
"mobile": "09122406750",
"firstName": "قاسم",
"lastName": "نصراللهي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyExpertiseClaim": "4645"
},
{
"nationalCode": "0670358118",
"mobile": "09124421539",
"firstName": "مجيد",
"lastName": "اميري",
"locations": [
{
"id": "210040",
"name": "شعبه شرق تهران(210040)"
}
],
"ThirdPartyExpertiseClaim": "3985",
"CarBodyExpertiseClaim": "3986"
},
{
"nationalCode": "0083730397",
"mobile": "09125759960",
"firstName": "مجيد",
"lastName": "کاظمي دولت سرا",
"locations": [
{
"id": "210120",
"name": "شعبه والفجر"
}
],
"ThirdPartyExpertiseClaim": "3992",
"CarBodyExpertiseClaim": "3991"
},
{
"nationalCode": "0493217789",
"mobile": "09126966943",
"firstName": "مصطفي",
"lastName": "محمدزاده قورقچي",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
}
],
"ThirdPartyExpertiseClaim": "3545",
"CarBodyExpertiseClaim": "3544"
},
{
"nationalCode": "0076988961",
"mobile": "09108357378",
"firstName": "مهدي",
"lastName": "روشن دل",
"locations": [
{
"id": "210110",
"name": "شعبه پونک"
}
],
"CarBodyExpertiseClaim": "3988",
"ThirdPartyExpertiseClaim": "3989"
},
{
"nationalCode": "0078209129",
"mobile": "09126038117",
"firstName": "مهدي",
"lastName": "شاملوفرد",
"locations": [
{
"id": "210050",
"name": "شعبه غرب تهران(210050)"
},
{
"id": "210110",
"name": "شعبه پونک"
}
],
"CarBodyExpertiseClaim": "73",
"ThirdPartyExpertiseClaim": "72"
}
]
}

143
scripts/fanavaran-auth.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
scripts/fanavaran-auth.sh <tejaratno|parsian>
Calls Fanavaran GetAppToken, then Login with the returned appToken.
Outputs the appToken and authenticationToken, and writes raw responses to:
files/fanavaran-auth/<client>/
Optional:
FANAVARAN_BASE_URL can override the default API Manager base URL.
USAGE
}
client="${1:-}"
if [[ -z "$client" || "$client" == "-h" || "$client" == "--help" ]]; then
usage
exit 0
fi
case "$client" in
tejaratno)
app_name='fanhab'
app_secret='5Fa@N#A2B'
fanavaran_username='fanhabUser'
fanavaran_password='Fan#@2U$3er'
corp_id='3539'
contract_id='263'
location='100'
;;
parsian)
app_name='ParsianService'
app_secret='P@r30@n$erv!ce'
fanavaran_username='ParsianServiceUser'
fanavaran_password='P@r30@n123'
corp_id='543'
contract_id='28'
location='210050'
;;
*)
printf 'Unknown Fanavaran client: %s\n\n' "$client" >&2
usage >&2
exit 1
;;
esac
base_url="${FANAVARAN_BASE_URL:-https://apimanager.iraneit.com/BimeApiManager/api}"
auth_dir="files/fanavaran-auth/$client"
mkdir -p "$auth_dir"
app_token_headers="$auth_dir/get-app-token.headers"
app_token_body="$auth_dir/get-app-token.body.json"
login_headers="$auth_dir/login.headers"
login_body="$auth_dir/login.body.json"
tokens_file="$auth_dir/tokens.env"
extract_header() {
local header_name="$1"
local header_file="$2"
awk -F': ' -v wanted="$header_name" '
tolower($1) == tolower(wanted) {
gsub(/\r/, "", $2)
print $2
exit
}
' "$header_file"
}
extract_authentication_token_from_body() {
local body_file="$1"
node -e '
const fs = require("fs");
const path = process.argv[1];
const body = fs.existsSync(path) ? fs.readFileSync(path, "utf8") : "";
try {
const json = JSON.parse(body || "{}");
console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || "");
} catch {
console.log("");
}
' "$body_file"
}
printf 'Fanavaran client: %s\n' "$client"
printf 'Base URL: %s\n\n' "$base_url"
printf '1. Calling GetAppToken...\n'
curl -sS -D "$app_token_headers" -o "$app_token_body" \
-X POST "$base_url/EITAuthentication/GetAppToken" \
-H "appname: $app_name" \
-H "secret: $app_secret" \
-H "Content-Length: 0"
app_token="$(extract_header "apptoken" "$app_token_headers")"
if [[ -z "$app_token" ]]; then
printf 'Failed to extract appToken from %s\n' "$app_token_headers" >&2
printf 'Response body is saved at %s\n' "$app_token_body" >&2
exit 1
fi
printf '2. Calling Login...\n'
curl -sS -D "$login_headers" -o "$login_body" \
-X POST "$base_url/EITAuthentication/Login" \
-H "appToken: $app_token" \
-H "userName: $fanavaran_username" \
-H "password: $fanavaran_password" \
-H "Content-Length: 0"
authentication_token="$(extract_header "authenticationtoken" "$login_headers")"
if [[ -z "$authentication_token" ]]; then
authentication_token="$(extract_authentication_token_from_body "$login_body")"
fi
if [[ -z "$authentication_token" ]]; then
printf 'Failed to extract authenticationToken from login response.\n' >&2
printf 'Headers: %s\n' "$login_headers" >&2
printf 'Body: %s\n' "$login_body" >&2
exit 1
fi
cat > "$tokens_file" <<TOKENS
FANAVARAN_CLIENT='$client'
FANAVARAN_BASE_URL='$base_url'
FANAVARAN_BIME_URL='$base_url/BimeApi/v2.0'
APP_TOKEN='$app_token'
AUTHENTICATION_TOKEN='$authentication_token'
CORP_ID='$corp_id'
CONTRACT_ID='$contract_id'
LOCATION='$location'
TOKENS
printf '\nDone.\n'
printf 'APP_TOKEN=%s\n' "$app_token"
printf 'AUTHENTICATION_TOKEN=%s\n' "$authentication_token"
printf 'CORP_ID=%s\n' "$corp_id"
printf 'CONTRACT_ID=%s\n' "$contract_id"
printf 'LOCATION=%s\n' "$location"
printf '\nSaved token variables: %s\n' "$tokens_file"
printf 'Saved raw GetAppToken response: %s, %s\n' "$app_token_headers" "$app_token_body"
printf 'Saved raw Login response: %s, %s\n' "$login_headers" "$login_body"

1111
scripts/fanavaran-flow-test.sh Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,920 @@
#!/usr/bin/env node
/*
* One-time script to replace mocked blameCases party vehicle inquiry data.
* THIRD_PARTY cases use Tejarat block inquiry; CAR_BODY cases use both
* Tejarat third-party block inquiry and car-body inquiry. All cases also
* refresh available party personal inquiries into inquiries.person.
*
* Defaults to DRY_RUN=true. Set DRY_RUN=false to write changes.
*/
const fs = require("fs");
const path = require("path");
const mongoose = require("mongoose");
const loadedEnvFiles = loadEnvFiles(process.env.ENV_FILE || ".env");
const LETTER_TO_NUMBER = {
"الف": 1,
"ب": 2,
"ت": 3,
"ج": 4,
"د": 5,
"س": 6,
"ص": 7,
"ط": 8,
"ع": 9,
"ق": 10,
"ل": 11,
"م": 12,
"ن": 13,
"و": 14,
"ه": 15,
"ی": 16,
"ر": 17,
"ک": 18,
"ژ": 19,
"پ": 20,
"ظ": 24,
"ض": 25,
"ز": 41,
"ش": 42,
"گ": 43,
"ث": 44,
D: 45,
S: 46,
"ح": 47,
"ف": 48,
};
const NUMBER_TO_LETTER = Object.fromEntries(
Object.entries(LETTER_TO_NUMBER).map(([letter, number]) => [String(number), letter]),
);
const config = {
mongoUri: requiredEnv("MONGO_URL", "MONGODB_URI", "DATABASE_URL"),
collectionName: process.env.BLAME_COLLECTION || "blameCases",
mongoDbName: process.env.MONGO_DB_NAME || "",
thirdPartyUrl:
process.env.TEJARAT_THIRD_PARTY_URL ||
"http://82.99.202.245:3027/block-inquiry-tejarat",
carBodyUrl:
process.env.TEJARAT_CAR_BODY_URL ||
"http://82.99.202.245:3027/block-inquiry-tejarat/badane",
personUrl:
process.env.TEJARAT_PERSON_URL ||
"http://82.99.202.245:3027/personal-inquiry/tejarat-no",
thirdPartyToken:
process.env.TEJARAT_THIRD_PARTY_TOKEN || process.env.TEJARAT_TOKEN || "",
carBodyToken:
process.env.TEJARAT_CAR_BODY_TOKEN || process.env.TEJARAT_TOKEN || "",
personToken:
process.env.TEJARAT_PERSON_TOKEN || process.env.TEJARAT_TOKEN || "",
rateLimitPerMinute: Number(process.env.RATE_LIMIT_PER_MINUTE || 5),
retryEnabled: String(process.env.RETRY_ENABLED ?? "true").toLowerCase() !== "false",
retryCount: Number(process.env.RETRY_COUNT || 3),
retryDelayMs: Number(process.env.RETRY_DELAY_MS || 2000),
dryRun: String(process.env.DRY_RUN ?? "true").toLowerCase() !== "false",
limit: process.env.LIMIT ? Number(process.env.LIMIT) : 0,
publicId: process.env.PUBLIC_ID || "",
};
let lastRequestAt = 0;
main().catch(async (error) => {
console.error("[fatal]", error && error.stack ? error.stack : error);
await mongoose.disconnect().catch(() => undefined);
process.exitCode = 1;
});
async function main() {
validateConfig();
console.log("script runned successfully");
console.log(
"[config] envFiles=" +
(loadedEnvFiles.length ? loadedEnvFiles.join(",") : "none") +
", dbName=" +
(config.mongoDbName || "from-url-or-driver-default") +
", collection=" +
config.collectionName +
", dryRun=" +
config.dryRun +
", rateLimitPerMinute=" +
config.rateLimitPerMinute +
", retryEnabled=" +
config.retryEnabled +
", retryCount=" +
config.retryCount,
);
await mongoose.connect(config.mongoUri, {
autoIndex: false,
dbName: config.mongoDbName || undefined,
});
const collection = mongoose.connection.collection(config.collectionName);
const query = {
type: { $in: ["THIRD_PARTY", "CAR_BODY"] },
};
if (config.publicId) query.publicId = config.publicId;
const totalDocs = await collection.countDocuments(query);
console.log(`total docs that we have to edit: ${totalDocs}`);
const cursor = collection
.find(query, {
projection: {
publicId: 1,
requestNo: 1,
type: 1,
parties: 1,
inquiries: 1,
},
})
.sort({ createdAt: 1, _id: 1 });
if (config.limit > 0) cursor.limit(config.limit);
const summary = {
docsSeen: 0,
docsChanged: 0,
partiesInquired: 0,
partiesSkipped: 0,
partiesFailed: 0,
personInquired: 0,
personSkipped: 0,
personFailed: 0,
};
for await (const doc of cursor) {
summary.docsSeen += 1;
const label = doc.publicId || doc.requestNo || String(doc._id);
const requestId = String(doc._id);
console.log(`currently inquiry for doc with ${label} and requestId ${requestId}`);
const parties = Array.isArray(doc.parties) ? clone(doc.parties) : [];
const inquiries = doc.inquiries && typeof doc.inquiries === "object" ? clone(doc.inquiries) : {};
let docChanged = false;
let inquiriesChanged = false;
for (let index = 0; index < parties.length; index += 1) {
const party = parties[index];
const partyLabel = `${label} parties[${index}] role=${party && party.role ? party.role : "-"}`;
const input = buildInquiryInputs(doc, party);
if (!input.ok) {
summary.partiesSkipped += 1;
console.warn(`[skip] ${partyLabel}: ${input.reason}`);
} else {
let nextParty = party;
let partyChanged = false;
for (const request of input.requests) {
console.log(
`[request] ${partyLabel} type=${request.type} body=${JSON.stringify(request.body)}`,
);
try {
const inquiryResponse = await inquiryWithRetry(request.type, request.body);
const successful = isInquirySuccessful(request.type, inquiryResponse);
console.log(
`[response] ${partyLabel} type=${request.type} successful=${successful} body=${JSON.stringify(inquiryResponse)}`,
);
if (!successful) {
summary.partiesFailed += 1;
continue;
}
nextParty = applyInquiryToParty(request.type, nextParty, inquiryResponse, input.plate);
summary.partiesInquired += 1;
partyChanged = true;
} catch (error) {
summary.partiesFailed += 1;
console.error(`[error] ${partyLabel} type=${request.type}: ${error.message}`);
}
}
if (partyChanged) {
parties[index] = nextParty;
docChanged = true;
}
}
const personInput = buildPersonInquiryInput(party);
if (!personInput.ok) {
summary.personSkipped += 1;
console.warn(`[skip] ${partyLabel} person: ${personInput.reason}`);
continue;
}
console.log(`[request] ${partyLabel} type=PERSON body=${JSON.stringify(personInput.body)}`);
try {
const personResponse = await inquiryWithRetry("PERSON", personInput.body);
const successful = isInquirySuccessful("PERSON", personResponse);
console.log(
`[response] ${partyLabel} type=PERSON successful=${successful} body=${JSON.stringify(personResponse)}`,
);
if (!successful) {
summary.personFailed += 1;
applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, personResponse);
inquiriesChanged = true;
continue;
}
applyPersonInquiryToCaseInquiries(
inquiries,
party,
index,
true,
normalizePersonResponse(personResponse),
);
summary.personInquired += 1;
inquiriesChanged = true;
} catch (error) {
summary.personFailed += 1;
applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, error);
inquiriesChanged = true;
console.error(`[error] ${partyLabel} type=PERSON: ${error.message}`);
}
}
if (!docChanged && !inquiriesChanged) {
console.log(`[doc] ${label} no changes`);
continue;
}
if (config.dryRun) {
console.log(`[dry-run] ${label} would update parties/inquiries`);
continue;
}
const updateSet = {
updatedAt: new Date(),
};
if (docChanged) updateSet.parties = parties;
if (inquiriesChanged) updateSet.inquiries = inquiries;
const result = await collection.updateOne(
{ _id: doc._id },
{
$set: updateSet,
},
);
summary.docsChanged += result.modifiedCount;
console.log(`[update] ${label} matched=${result.matchedCount} modified=${result.modifiedCount}`);
}
await mongoose.disconnect();
console.log(`[done] ${JSON.stringify(summary)}`);
}
function buildInquiryInputs(doc, party) {
if (!party || typeof party !== "object") return { ok: false, reason: "party is empty" };
if (!party.vehicle || typeof party.vehicle !== "object") {
return { ok: false, reason: "party.vehicle is missing" };
}
const plate = extractPlate(party);
const nationalCode =
cleanString(party.person && party.person.nationalCodeOfInsurer) ||
cleanString(party.person && party.person.nationalCodeOfDriver);
if (!plate) return { ok: false, reason: "Plk1/Plk2/Plk3/PlkSrl not found" };
if (!nationalCode) return { ok: false, reason: "nationalCodeOfInsurer/nationalCodeOfDriver missing" };
const serialLetter = NUMBER_TO_LETTER[String(plate.Plk2)] || cleanString(plate.Plk2);
if (!serialLetter) return { ok: false, reason: `no Persian letter mapping for Plk2=${plate.Plk2}` };
if (doc.type === "THIRD_PARTY") {
return {
ok: true,
plate,
requests: [buildThirdPartyRequest(plate, serialLetter, nationalCode)],
};
}
if (doc.type === "CAR_BODY") {
return {
ok: true,
plate,
requests: [
buildThirdPartyRequest(plate, serialLetter, nationalCode),
buildCarBodyRequest(plate, serialLetter, nationalCode),
],
};
}
return { ok: false, reason: `unsupported type=${doc.type}` };
}
function buildThirdPartyRequest(plate, serialLetter, nationalCode) {
return {
type: "THIRD_PARTY",
body: {
leftTwoDigits: String(plate.Plk1),
serialLetter,
threeDigits: String(plate.Plk3),
rightTwoDigits: String(plate.PlkSrl),
nationalCode,
},
};
}
function buildCarBodyRequest(plate, serialLetter, nationalCode) {
return {
type: "CAR_BODY",
body: {
part1: toNumber(plate.Plk1),
part2: serialLetter,
part3: toNumber(plate.Plk3),
part4: toNumber(plate.PlkSrl),
nationalCode,
},
};
}
function buildPersonInquiryInput(party) {
if (!party || typeof party !== "object") return { ok: false, reason: "party is empty" };
const person = party.person && typeof party.person === "object" ? party.person : null;
if (!person) return { ok: false, reason: "party.person is missing" };
const nationalCode =
cleanString(person.nationalCodeOfInsurer) ||
cleanString(person.nationalCodeOfDriver);
const birthDate = firstPresent(
person.insurerBirthday,
person.driverBirthday,
person.birthday,
);
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
if (!nationalCode) {
return { ok: false, reason: "nationalCodeOfInsurer/nationalCodeOfDriver missing" };
}
if (!gregorianBirthdate) {
return {
ok: false,
reason: `invalid insurerBirthday/driverBirthday=${cleanString(birthDate)}`,
};
}
return {
ok: true,
body: {
nationalCode,
birthdate: gregorianBirthdate,
},
};
}
function parsePlateId(plateId) {
const value = cleanString(plateId);
if (!value) return null;
const parts = value.split("-").map((part) => normalizePlateNumber(part));
if (parts.length !== 4) return null;
const thirdPartIsLetter = LETTER_TO_NUMBER[parts[2]] !== undefined;
const fourthPartIsLetter = LETTER_TO_NUMBER[parts[3]] !== undefined;
if (thirdPartIsLetter) {
return {
Plk1: parts[1],
Plk2: String(LETTER_TO_NUMBER[parts[2]]),
Plk3: parts[3],
PlkSrl: parts[0],
};
}
if (fourthPartIsLetter) {
return {
Plk1: parts[2],
Plk2: String(LETTER_TO_NUMBER[parts[3]]),
Plk3: parts[1],
PlkSrl: parts[0],
};
}
return null;
}
function extractPlate(party) {
const plateFromPlateId = parsePlateId(party.vehicle && party.vehicle.plateId);
if (plateFromPlateId) return plateFromPlateId;
const candidates = [
party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.mapped,
party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.raw,
party.vehicle && party.vehicle.inquiry,
party.vehicle,
].filter(Boolean);
for (const candidate of candidates) {
const Plk1 = firstPresent(candidate.Plk1, candidate.platePartOne);
const Plk2 = firstPresent(candidate.Plk2, candidate.plateLetterid, candidate.plateLetterId);
const Plk3 = firstPresent(candidate.Plk3, candidate.platePartThree);
const PlkSrl = firstPresent(candidate.PlkSrl, candidate.plkSrl, candidate.plateSerialNumber);
if (
Plk1 !== undefined &&
Plk2 !== undefined &&
Plk3 !== undefined &&
PlkSrl !== undefined
) {
return {
Plk1: normalizePlateNumber(Plk1),
Plk2: normalizePlateNumber(Plk2),
Plk3: normalizePlateNumber(Plk3),
PlkSrl: normalizePlateNumber(PlkSrl),
};
}
}
return null;
}
async function inquiryWithRetry(type, body) {
const endpoint = getInquiryEndpoint(type);
const url = endpoint.url;
const token = endpoint.token;
const accept = endpoint.accept;
let lastError;
const maxAttempts = config.retryEnabled ? config.retryCount : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
await waitForRateLimit();
try {
console.log("[http] " + type + " attempt=" + attempt + "/" + maxAttempts);
return await postJson(url, token, body, accept);
} catch (error) {
lastError = error;
console.error(`[retry] ${type} attempt=${attempt} failed: ${error.message}`);
if (attempt < maxAttempts) {
await sleep(config.retryDelayMs * attempt);
}
}
}
throw lastError;
}
async function postJson(url, token, body, accept = "application/json") {
const response = await fetch(url, {
method: "POST",
headers: {
accept,
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify(body),
});
const text = await response.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = text;
}
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
}
function isInquirySuccessful(type, response) {
if (type === "CAR_BODY") return response && response.isSuccess === true && response.data;
if (type === "PERSON") return response && response.status === 200 && response.data;
return response && response.resultStatus === true;
}
function getInquiryEndpoint(type) {
if (type === "CAR_BODY") {
return { url: config.carBodyUrl, token: config.carBodyToken, accept: "application/json" };
}
if (type === "PERSON") {
return { url: config.personUrl, token: config.personToken, accept: "*/*" };
}
return { url: config.thirdPartyUrl, token: config.thirdPartyToken, accept: "application/json" };
}
function applyInquiryToParty(type, party, response, originalPlate) {
if (type === "CAR_BODY") return applyCarBodyInquiryToParty(party, response, originalPlate);
return applyThirdPartyInquiryToParty(party, response, originalPlate);
}
function applyThirdPartyInquiryToParty(party, response, originalPlate) {
const next = clone(party);
if (!next.vehicle) next.vehicle = {};
if (!next.insurance) next.insurance = {};
const mapped = normalizeThirdPartyResponse(response, originalPlate);
const plateId = buildPlateId(originalPlate);
next.vehicle.plateId = plateId || next.vehicle.plateId;
next.vehicle.inquiry = {
source: "TEJARAT_BLOCK_INQUIRY",
raw: response,
mapped,
refreshedAt: new Date().toISOString(),
};
if (party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.carBody) {
next.vehicle.inquiry.carBody = party.vehicle.inquiry.carBody;
}
next.vehicle.name = mapped.vehiclePersianName || mapped.MapTypNam || "اطلاعات این گزینه در استعلام موجود نیست";
next.vehicle.type = mapped.persianCarType || mapped.MapUsageName || next.vehicle.type;
next.insurance.policyNumber =
mapped.LastCompanyDocumentNumber || mapped.insuranceNumber || next.insurance.policyNumber;
next.insurance.company = mapped.companyPersianName || next.insurance.company;
next.insurance.financialCeiling = mapped.financeCoverage || next.insurance.financialCeiling;
next.insurance.startDate = mapped.persianStartDate || next.insurance.startDate;
next.insurance.endDate = mapped.persianEndDate || next.insurance.endDate;
return next;
}
function applyCarBodyInquiryToParty(party, response, originalPlate) {
const next = clone(party);
if (!next.vehicle) next.vehicle = {};
if (!next.insurance) next.insurance = {};
const mapped = normalizeCarBodyResponse(response, originalPlate);
const plateId = buildPlateId(originalPlate);
next.vehicle.plateId = plateId || next.vehicle.plateId;
if (!next.vehicle.inquiry || typeof next.vehicle.inquiry !== "object") {
next.vehicle.inquiry = {};
}
next.vehicle.inquiry.carBody = {
source: "TEJARAT_CAR_BODY_INQUIRY",
raw: response,
mapped,
refreshedAt: new Date().toISOString(),
};
next.vehicle.name = mapped.vehicleSystemTitle || next.vehicle.name;
next.vehicle.type = mapped.vehicleGroupTitle || next.vehicle.type;
next.insurance.carBodyInsurance = {
policyNumber: mapped.policyNumber,
companyId: mapped.companyId,
companyName: mapped.CompanyName,
insurerName: mapped.insurerName,
insurerNationalCode: mapped.insurerNationalCode,
ownerNationalCode: mapped.ownerNationalCode,
chassisNumber: mapped.chassisNumber,
vin: mapped.vin,
motorNumber: mapped.motorNumber,
vehicleGroup: mapped.vehicleGroupTitle,
vehicleSystem: mapped.vehicleSystemTitle,
startDate: mapped.StartDate,
endDate: mapped.EndDate,
issueDate: mapped.IssueDate,
noLossYearsCount: mapped.noLossYearsCount,
lossDocuments: Array.isArray(mapped.lossDocuments) ? mapped.lossDocuments : [],
hasEndorsement: mapped.hasEndorsement,
};
return next;
}
function applyPersonInquiryToCaseInquiries(inquiries, party, index, has, data, error) {
if (!inquiries.person || typeof inquiries.person !== "object") {
inquiries.person = {};
}
const existingData =
inquiries.person.data && typeof inquiries.person.data === "object" && !Array.isArray(inquiries.person.data)
? inquiries.person.data
: {};
const existingError =
inquiries.person.error && typeof inquiries.person.error === "object" && !Array.isArray(inquiries.person.error)
? inquiries.person.error
: {};
const roleKey = party && party.role ? party.role : `party_${index}`;
const nextData = { ...existingData };
const nextError = { ...existingError };
if (has) {
nextData[roleKey] = data || {};
delete nextError[roleKey];
} else {
nextError[roleKey] = normalizeInquiryError(error);
}
inquiries.person = {
has: Object.keys(nextData).length > 0,
data: nextData,
...(Object.keys(nextError).length > 0 ? { error: nextError } : {}),
updatedAt: new Date(),
};
}
function normalizePersonResponse(response) {
return response && response.data ? response.data : response;
}
function normalizeInquiryError(error) {
if (!error) return undefined;
return {
message: error.message || String(error),
status: error.status,
data: error.data,
};
}
function buildPlateId(plate) {
if (!plate) return "";
const serialLetter = NUMBER_TO_LETTER[String(plate.Plk2)] || cleanString(plate.Plk2);
if (!serialLetter) return "";
return (
cleanString(plate.PlkSrl) +
"-" +
cleanString(plate.Plk1) +
"-" +
serialLetter +
"-" +
cleanString(plate.Plk3)
);
}
function normalizeThirdPartyResponse(response, originalPlate) {
return {
...response,
Plk1: toNumber(originalPlate.Plk1),
Plk2: toNumber(originalPlate.Plk2),
Plk3: toNumber(originalPlate.Plk3),
PlkSrl: toNumber(originalPlate.PlkSrl),
CompanyName: response.companyPersianName,
CompanyCode: response.companyId,
LastCompanyDocumentNumber: response.lastCompanyInsuranceNumber || response.insuranceNumber,
FinancialCvrCptl: response.financeCoverage,
IssueDate: response.hIsuDte || response.persianStartDate,
SatrtDate: response.persianStartDate,
EndDate: response.persianEndDate,
MapTypNam: response.vehiclePersianName,
MapUsageName: response.MapUsageName || response.persianCarType,
UsageField: response.persianCarType,
UsageCode: response.usgCod,
VehicleSystemCode: response.vehSysCod,
CarGroupCode: response.carGrpCod,
EdrsJson: Array.isArray(response.edrSes) ? JSON.stringify(response.edrSes) : response.EdrsJson,
InsuranceFullName: response.fullname,
};
}
function normalizeCarBodyResponse(response, originalPlate) {
const data = response.data || {};
return {
...data,
Plk1: toNumber(originalPlate.Plk1),
Plk2: toNumber(originalPlate.Plk2),
Plk3: toNumber(originalPlate.Plk3),
PlkSrl: toNumber(originalPlate.PlkSrl),
policyNumber: data.printNumber,
CompanyName: data.companyName,
CompanyCode: data.companyId,
LastCompanyDocumentNumber: data.printNumber,
InsuranceFullName: data.insurerName,
IssueDate: data.issueDate,
StartDate: data.beginDate,
SatrtDate: data.beginDate,
EndDate: data.endDate,
EngineNumberField: data.motorNumber,
MtrNum: data.motorNumber,
ChassisNumberField: data.chassisNumber,
ShsNum: data.chassisNumber,
VinNumberField: data.vin,
MapTypNam: data.vehicleGroupTitle,
isSuccess: response.isSuccess,
statusCode: response.statusCode,
message: response.message,
};
}
function jalaliToGregorianDate(input) {
if (input === null || input === undefined) return null;
const raw = normalizeDigits(typeof input === "number" ? String(input) : String(input).trim());
if (!raw) return null;
let year = 0;
let month = 0;
let day = 0;
const separated = raw.match(/^(\d{4})[\-/](\d{1,2})[\-/](\d{1,2})$/);
if (separated) {
year = parseInt(separated[1], 10);
month = parseInt(separated[2], 10);
day = parseInt(separated[3], 10);
} else {
const digits = raw.replace(/\D/g, "");
if (digits.length !== 8) return null;
year = parseInt(digits.slice(0, 4), 10);
month = parseInt(digits.slice(4, 6), 10);
day = parseInt(digits.slice(6, 8), 10);
}
if (!year || !month || !day) return null;
if (year >= 1900) {
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
const result = `${year}-${mm}-${dd}`;
return isNaN(new Date(result).getTime()) ? null : result;
}
return jalaliPartsToGregorian(year, month, day);
}
function jalaliPartsToGregorian(jYear, jMonth, jDay) {
const jy = jYear - 979;
const jm = jMonth - 1;
const jd = jDay - 1;
let jDayNo =
365 * jy + Math.floor(jy / 33) * 8 + Math.floor(((jy % 33) + 3) / 4);
const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
for (let i = 0; i < jm; i += 1) {
jDayNo += jalaliMonthDays[i];
}
jDayNo += jd;
let gDayNo = jDayNo + 79;
let gy = 1600 + 400 * Math.floor(gDayNo / 146097);
gDayNo %= 146097;
let leap = true;
if (gDayNo >= 36525) {
gDayNo -= 1;
gy += 100 * Math.floor(gDayNo / 36524);
gDayNo %= 36524;
if (gDayNo >= 365) gDayNo += 1;
else leap = false;
}
gy += 4 * Math.floor(gDayNo / 1461);
gDayNo %= 1461;
if (gDayNo >= 366) {
leap = false;
gDayNo -= 1;
gy += Math.floor(gDayNo / 365);
gDayNo %= 365;
}
const gregorianMonthDays = [
31,
leap ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let gm = 0;
for (let i = 0; i < 12; i += 1) {
if (gDayNo < gregorianMonthDays[i]) {
gm = i + 1;
break;
}
gDayNo -= gregorianMonthDays[i];
}
const gd = gDayNo + 1;
const mm = String(gm).padStart(2, "0");
const dd = String(gd).padStart(2, "0");
const result = `${gy}-${mm}-${dd}`;
return isNaN(new Date(result).getTime()) ? null : result;
}
async function waitForRateLimit() {
const minDelayMs = Math.ceil(60000 / config.rateLimitPerMinute);
const elapsed = Date.now() - lastRequestAt;
if (lastRequestAt > 0 && elapsed < minDelayMs) {
const waitMs = minDelayMs - elapsed;
console.log(`[rate-limit] waiting ${waitMs}ms`);
await sleep(waitMs);
}
lastRequestAt = Date.now();
}
function validateConfig() {
if (!config.mongoUri) throw new Error("MONGO_URL is required");
if (!Number.isFinite(config.rateLimitPerMinute) || config.rateLimitPerMinute <= 0) {
throw new Error("RATE_LIMIT_PER_MINUTE must be a positive number");
}
if (!Number.isFinite(config.retryCount) || config.retryCount <= 0) {
throw new Error("RETRY_COUNT must be a positive number");
}
if (!config.thirdPartyToken) {
throw new Error("TEJARAT_THIRD_PARTY_TOKEN or TEJARAT_TOKEN is required");
}
if (!config.carBodyToken) {
throw new Error("TEJARAT_CAR_BODY_TOKEN or TEJARAT_TOKEN is required");
}
if (!config.personToken) {
throw new Error("TEJARAT_PERSON_TOKEN or TEJARAT_TOKEN is required");
}
}
function loadEnvFiles(filePath) {
const candidates = path.isAbsolute(filePath)
? [filePath]
: [
path.resolve(process.cwd(), filePath),
path.resolve(__dirname, "..", filePath),
];
const loaded = [];
for (const candidate of [...new Set(candidates)]) {
if (!fs.existsSync(candidate)) continue;
loadEnvFile(candidate);
loaded.push(candidate);
}
return loaded;
}
function loadEnvFile(filePath) {
const resolved = path.resolve(process.cwd(), filePath);
if (!fs.existsSync(resolved)) return;
const lines = fs.readFileSync(resolved, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const equalIndex = trimmed.indexOf("=");
if (equalIndex === -1) continue;
const key = trimmed.slice(0, equalIndex).trim();
let value = trimmed.slice(equalIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (key && process.env[key] === undefined) process.env[key] = value;
}
}
function requiredEnv(...names) {
for (const name of names) {
if (process.env[name]) return process.env[name];
}
return "";
}
function firstPresent(...values) {
return values.find((value) => value !== undefined && value !== null && value !== "");
}
function normalizePlateNumber(value) {
const cleaned = normalizeDigits(cleanString(value));
return cleaned === "" ? value : cleaned;
}
function normalizeDigits(value) {
return cleanString(value).replace(/./g, (char) => {
const code = char.charCodeAt(0);
if (code >= 0x06f0 && code <= 0x06f9) return String(code - 0x06f0);
if (code >= 0x0660 && code <= 0x0669) return String(code - 0x0660);
return char;
});
}
function toNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : value;
}
function cleanString(value) {
return value === undefined || value === null ? "" : String(value).trim();
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,512 @@
/**
* One-time seed for Parsian (clientCode=8) Tehran branches + file reviewers + file makers.
*
* Usage (before starting the app):
* npm run seed:parsian-tehran
*
* Optional env:
* SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD=Parsian@724
*
* Backward-compatible env alias:
* SEED_FIELD_EXPERT_DEFAULT_PASSWORD=Parsian@724
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import * as crypto from "node:crypto";
import mongoose, { Schema, Types } from "mongoose";
type BranchSeed = {
code: string;
name: string;
fullName?: string;
city: string;
state: string;
address: string;
phoneNumber?: string;
isActive?: boolean;
};
type ExpertLocationSeed = {
id: string;
name: string;
};
type FileReviewerSeed = {
nationalCode: string;
mobile?: string;
firstName: string;
lastName: string;
locations: ExpertLocationSeed[];
ThirdPartyExpertiseClaim?: string;
CarBodyExpertiseClaim?: string;
};
type FileMakerSeed = {
nationalCode: string;
mobile?: string;
firstName: string;
lastName: string;
locations: ExpertLocationSeed[];
ThirdPartyClaimExpertId?: string;
CarBodyClaimExpertId?: string;
};
function stripQuotes(value: string): string {
const trimmed = value.trim();
if (
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
(trimmed.startsWith('"') && trimmed.endsWith('"'))
) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function stripInlineComment(value: string): string {
const hashIdx = value.indexOf(" #");
return hashIdx === -1 ? value : value.slice(0, hashIdx).trim();
}
function expandEnvValue(value: string, env: NodeJS.ProcessEnv): string {
return value.replace(/\$\{([^}]+)\}/g, (_, key: string) => env[key] ?? "");
}
function loadEnvFile() {
const envPath = join(process.cwd(), ".env");
if (!existsSync(envPath)) return;
const raw: Record<string, string> = {};
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const idx = trimmed.indexOf("=");
if (idx === -1) continue;
const key = trimmed.slice(0, idx).trim();
const value = stripInlineComment(trimmed.slice(idx + 1).trim());
raw[key] = value;
}
for (const [key, value] of Object.entries(raw)) {
if (process.env[key]) continue;
process.env[key] = stripQuotes(value);
}
for (let pass = 0; pass < 5; pass++) {
let changed = false;
for (const key of Object.keys(process.env)) {
const current = process.env[key];
if (!current || !current.includes("${")) continue;
const expanded = expandEnvValue(stripQuotes(current), process.env);
if (expanded !== current) {
process.env[key] = expanded;
changed = true;
}
}
if (!changed) break;
}
for (const key of Object.keys(process.env)) {
const value = process.env[key];
if (value) process.env[key] = stripQuotes(value);
}
}
function resolveMongoUri(): string {
const uri = process.env.MONGO_URI?.trim();
if (!uri) {
throw new Error("MONGO_URI is not set in .env");
}
if (!uri.startsWith("mongodb://") && !uri.startsWith("mongodb+srv://")) {
throw new Error(
`Invalid MONGO_URI after env expansion: "${uri.slice(0, 40)}..."`,
);
}
return uri;
}
async function ensureUserIndexes(collection: mongoose.Collection) {
const indexes = await collection.indexes();
const emailIndex = indexes.find((idx) => idx.key?.email === 1);
if (emailIndex && !emailIndex.sparse) {
await collection.dropIndex(emailIndex.name);
console.log(`Dropped legacy non-sparse index: ${emailIndex.name}`);
}
await collection.createIndex({ email: 1 }, { unique: true, sparse: true });
await collection.createIndex(
{ clientKey: 1, nationalCode: 1 },
{ unique: true, sparse: true },
);
}
function hashPassword(password: string): Promise<string> {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16).toString("hex");
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(`${salt}:${derivedKey.toString("hex")}`);
});
});
}
function normalizeLocations(locations: ExpertLocationSeed[]): ExpertLocationSeed[] {
const deduped = new Map<string, ExpertLocationSeed>();
for (const location of locations ?? []) {
const id = String(location?.id ?? "").trim();
const name = String(location?.name ?? "").trim();
if (!id || !name || deduped.has(id)) continue;
deduped.set(id, { id, name });
}
return [...deduped.values()];
}
const ClientSchema = new Schema(
{
clientName: { type: Object, required: true },
clientCode: { type: Number, required: true },
useExpertMode: { type: String, required: true },
},
{ collection: "clients", versionKey: false },
);
const BranchSchema = new Schema(
{
clientKey: { type: Schema.Types.ObjectId, required: true, index: true },
name: { type: String, required: true },
code: { type: String, required: true },
city: { type: String, required: true },
state: { type: String, required: true },
address: { type: String, required: true },
phoneNumber: { type: String },
isActive: { type: Boolean, default: true },
},
{ collection: "branches", versionKey: false, timestamps: true },
);
BranchSchema.index({ clientKey: 1, code: 1 }, { unique: true });
const ExpertLocationSchema = new Schema(
{
id: { type: String, required: true },
name: { type: String, required: true },
},
{ _id: false, id: false, versionKey: false },
);
const FileReviewerSchema = new Schema(
{
firstName: { type: String, required: true },
lastName: { type: String, required: true },
email: { type: String, unique: true, sparse: true },
username: { type: String },
nationalCode: { type: String, index: true, sparse: true },
clientKey: { type: Schema.Types.ObjectId, index: true },
branchId: { type: Schema.Types.ObjectId, index: true },
locations: { type: [ExpertLocationSchema], default: [] },
password: { type: String, required: true },
mobile: { type: String },
phone: { type: String },
role: { type: String, default: "file_reviewer" },
otp: { type: String, default: "" },
expertCode: { type: String, required: false },
ThirdPartyExpertiseClaim: { type: String, required: false },
CarBodyExpertiseClaim: { type: String, required: false },
},
{ collection: "file-reviewer", versionKey: false, timestamps: true },
);
FileReviewerSchema.index(
{ clientKey: 1, nationalCode: 1 },
{ unique: true, sparse: true },
);
const FileMakerSchema = new Schema(
{
firstName: { type: String, required: true },
lastName: { type: String, required: true },
email: { type: String, unique: true, sparse: true },
username: { type: String },
nationalCode: { type: String, index: true, sparse: true },
clientKey: { type: Schema.Types.ObjectId, index: true },
branchId: { type: Schema.Types.ObjectId, index: true },
locations: { type: [ExpertLocationSchema], default: [] },
password: { type: String, required: true },
mobile: { type: String },
phone: { type: String },
role: { type: String, default: "file_maker" },
otp: { type: String, default: "" },
expertCode: { type: String, required: false },
ThirdPartyClaimExpertId: { type: String, required: false },
CarBodyClaimExpertId: { type: String, required: false },
},
{ collection: "file-maker", versionKey: false, timestamps: true },
);
FileMakerSchema.index(
{ clientKey: 1, nationalCode: 1 },
{ unique: true, sparse: true },
);
function resolveSeedLocations(
seedLocations: ExpertLocationSeed[],
branchMetaByCode: Map<string, { _id: Types.ObjectId; name: string }>,
) {
const resolved: { id: string; name: string; branchId: Types.ObjectId }[] = [];
const skippedCodes: string[] = [];
for (const location of normalizeLocations(seedLocations)) {
const branch = branchMetaByCode.get(location.id);
if (!branch) {
skippedCodes.push(location.id);
continue;
}
resolved.push({
id: location.id,
name: branch.name || location.name,
branchId: branch._id,
});
}
return {
resolved,
skippedCodes,
primaryBranchId: resolved[0]?.branchId,
};
}
async function upsertRoleUsers({
label,
seeds,
model,
clientKey,
hashedPassword,
branchMetaByCode,
role,
codeFields,
}: {
label: string;
seeds: Array<Record<string, any>>;
model: mongoose.Model<any>;
clientKey: Types.ObjectId;
hashedPassword: string;
branchMetaByCode: Map<string, { _id: Types.ObjectId; name: string }>;
role: string;
codeFields: string[];
}) {
let created = 0;
let updated = 0;
let skipped = 0;
for (const seed of seeds) {
const { resolved, skippedCodes, primaryBranchId } = resolveSeedLocations(
seed.locations,
branchMetaByCode,
);
if (skippedCodes.length > 0) {
console.warn(
`Skipping unknown ${label} locations for ${seed.nationalCode}: ${skippedCodes.join(
", ",
)}`,
);
}
if (!primaryBranchId || resolved.length === 0) {
console.warn(
`Skipping ${label} ${seed.nationalCode}: no valid branch locations remained`,
);
skipped++;
continue;
}
const setPayload: Record<string, unknown> = {
firstName: seed.firstName,
lastName: seed.lastName,
username: seed.nationalCode,
nationalCode: seed.nationalCode,
clientKey,
branchId: primaryBranchId,
locations: resolved.map(({ id, name }) => ({ id, name })),
role,
otp: "",
};
if (seed.mobile) {
setPayload.mobile = seed.mobile;
}
const unsetPayload: Record<string, ""> = {
expertCode: "",
};
for (const field of codeFields) {
if (seed[field]) {
setPayload[field] = seed[field];
} else {
unsetPayload[field] = "";
}
}
const existing = await model.findOne({
clientKey,
nationalCode: seed.nationalCode,
});
if (existing) {
await model.updateOne(
{ _id: existing._id },
{
$set: {
...setPayload,
password: existing.password,
},
$unset: unsetPayload,
},
);
updated++;
} else {
await model.create({
...setPayload,
password: hashedPassword,
});
created++;
}
}
return { created, updated, skipped };
}
async function main() {
loadEnvFile();
const mongoUri = resolveMongoUri();
const dataDir = join(process.cwd(), "scripts/data/parsian-tehran");
const branchesFile = JSON.parse(
readFileSync(join(dataDir, "branches.json"), "utf8"),
) as { clientCode: number; branches: BranchSeed[] };
const fileReviewersFile = JSON.parse(
readFileSync(join(dataDir, "file-reviewers.json"), "utf8"),
) as { clientCode: number; fileReviewers: FileReviewerSeed[] };
const fileMakersFile = JSON.parse(
readFileSync(join(dataDir, "file-makers.json"), "utf8"),
) as { clientCode: number; fileMakers: FileMakerSeed[] };
if (
branchesFile.clientCode !== fileReviewersFile.clientCode ||
branchesFile.clientCode !== fileMakersFile.clientCode
) {
throw new Error("Seed data clientCode mismatch between branch/reviewer/maker files");
}
const defaultPassword =
process.env.SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD ??
process.env.SEED_FIELD_EXPERT_DEFAULT_PASSWORD ??
"123321";
const hashedPassword = await hashPassword(defaultPassword);
await mongoose.connect(mongoUri, {
tls: process.env.MONGO_TLS === "true",
tlsAllowInvalidCertificates:
process.env.MONGO_TLS_ALLOW_INVALID_CERTS === "true",
});
const Client = mongoose.model("ClientSeedClient", ClientSchema);
const Branch = mongoose.model("ClientSeedBranch", BranchSchema);
const FileReviewer = mongoose.model("ClientSeedFileReviewer", FileReviewerSchema);
const FileMaker = mongoose.model("ClientSeedFileMaker", FileMakerSchema);
await ensureUserIndexes(FileReviewer.collection);
await ensureUserIndexes(FileMaker.collection);
const client = await Client.findOne({
clientCode: branchesFile.clientCode,
}).lean();
if (!client?._id) {
throw new Error(
`Client with clientCode=${branchesFile.clientCode} not found in database`,
);
}
const clientKey = new Types.ObjectId(String(client._id));
const branchMetaByCode = new Map<string, { _id: Types.ObjectId; name: string }>();
let branchesCreated = 0;
let branchesUpdated = 0;
for (const branch of branchesFile.branches) {
const existing = await Branch.findOne({
clientKey,
code: branch.code,
});
const payload = {
clientKey,
name: branch.name,
code: branch.code,
city: branch.city,
state: branch.state,
address: branch.address,
phoneNumber: branch.phoneNumber,
isActive: branch.isActive ?? true,
};
if (existing) {
await Branch.updateOne({ _id: existing._id }, { $set: payload });
branchMetaByCode.set(branch.code, {
_id: existing._id as Types.ObjectId,
name: branch.name,
});
branchesUpdated++;
} else {
const created = await Branch.create(payload);
branchMetaByCode.set(branch.code, {
_id: created._id as Types.ObjectId,
name: branch.name,
});
branchesCreated++;
}
}
const fileReviewersResult = await upsertRoleUsers({
label: "file-reviewer",
seeds: fileReviewersFile.fileReviewers,
model: FileReviewer,
clientKey,
hashedPassword,
branchMetaByCode,
role: "file_reviewer",
codeFields: ["ThirdPartyExpertiseClaim", "CarBodyExpertiseClaim"],
});
const fileMakersResult = await upsertRoleUsers({
label: "file-maker",
seeds: fileMakersFile.fileMakers,
model: FileMaker,
clientKey,
hashedPassword,
branchMetaByCode,
role: "file_maker",
codeFields: ["ThirdPartyClaimExpertId", "CarBodyClaimExpertId"],
});
console.log("Parsian Tehran seed completed.");
console.log({
clientCode: branchesFile.clientCode,
clientKey: String(clientKey),
branchesCreated,
branchesUpdated,
fileReviewersCreated: fileReviewersResult.created,
fileReviewersUpdated: fileReviewersResult.updated,
fileReviewersSkipped: fileReviewersResult.skipped,
fileMakersCreated: fileMakersResult.created,
fileMakersUpdated: fileMakersResult.updated,
fileMakersSkipped: fileMakersResult.skipped,
defaultPassword,
loginHint:
"Use nationalCode + password on POST /actor/login with role file_reviewer or file_maker",
});
await mongoose.disconnect();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Quick manual verification for plate normalization.
*
* Usage: node scripts/verify-plate-normalization.mjs
*
* Demonstrates that:
* 339ه‍77 (ه + U+200D) -> 339ه77 -> letter ه -> code 15
* 339ي77 (Arabic ي) -> 339ی77 -> letter ی -> code 16
*/
// ---------- paste the pure function here so no build needed ----------
function normalizePlateText(text) {
if (!text) return text;
let result = String(text);
result = result.normalize("NFKC");
result = result.replace(/[\u200C\u200D\u200E\u200F\uFEFF]/g, "");
result = result.replace(/\u064A/g, "\u06CC"); // Arabic ي → Persian ی
result = result.trim();
return result;
}
// ---------- the canonical plate-letter → code mapping ----------
const PLATE_LETTER_MAP = {
الف: 1, ب: 2, پ: 3, ج: 4, د: 5,
س: 6, ص: 7, ط: 8, ع: 9, ق: 10,
ل: 11, م: 12, ن: 13, و: 14, ه: 15,
ی: 16, ک: 17, ژ: 18, ت: 19, ث: 20,
ز: 21, ش: 22, ف: 23, گ: 24,
};
function extractAndMap(plateString) {
const normalized = normalizePlateText(plateString);
// Simple parser: digits | letter | digits (left+right digits, 1 letter)
const match = normalized.match(/^(\d+)([^\d]+)(\d+)$/);
if (!match) return { input: plateString, normalized, error: "no match" };
const [, leftDigits, letterRaw, rightDigits] = match;
const letter = normalizePlateText(letterRaw);
const code = PLATE_LETTER_MAP[letter];
return {
input: plateString,
normalized,
leftDigits,
letter,
rightDigits,
code,
ok: code !== undefined,
};
}
// ---------- test cases ----------
const cases = [
{ desc: "ه + ZWJ (the original bug)", plate: "339ه\u200D77" },
{ desc: "Arabic ي → Persian ی", plate: "339ي77" },
{ desc: "ه + ZWNJ", plate: "339ه\u200C77" },
{ desc: "RTL mark around letter", plate: "339\u200Fه\u200E77" },
{ desc: "BOM + ZWJ combined", plate: "\uFEFF339ه\u200D77" },
{ desc: "clean letter (no junk)", plate: "339ه77" },
{ desc: "full plate: الف", plate: "11الف22" },
{ desc: "full plate: ی", plate: "55ی99" },
];
console.log("Plate Normalization Verification\n");
console.log("=".repeat(72));
let allPassed = true;
for (const { desc, plate } of cases) {
const r = extractAndMap(plate);
const status = r.ok ? "PASS" : "FAIL";
if (!r.ok) allPassed = false;
console.log(`\n[${status}] ${desc}`);
console.log(` Input: ${JSON.stringify(plate)}`);
console.log(` Normalized: ${JSON.stringify(r.normalized)}`);
if (r.letter) {
console.log(` Letter: ${r.letter} (U+${r.letter.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")})`);
}
console.log(` Code: ${r.code ?? "undefined"}`);
}
console.log("\n" + "=".repeat(72));
console.log(allPassed ? "\n All tests PASSED ✓" : "\n Some tests FAILED ✗");
process.exit(allPassed ? 0 : 1);

View File

@@ -2,7 +2,10 @@
export enum WorkflowStep {
CREATED = "CREATED",
// ---------- CAR_BODY only (no confession; accident type form) ----------
CAR_BODY_ACCIDENT_TYPE = "CAR_BODY_ACCIDENT_TYPE",
// ---------- First party ----------
FIRST_BLAME_CONFESSION = "FIRST_BLAME_CONFESSION",
FIRST_VIDEO = "FIRST_VIDEO",

View File

@@ -1,19 +1,39 @@
//! NEW
export enum CaseStatus {
OPEN = "OPEN",
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
AUTO_CLOSED = "AUTO_CLOSED"
}
OPEN = "OPEN",
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
/**
* FileMaker has collected all signatures; the file is sealed and waiting
* for a FileReviewer to complete it (accident fields → capture → video).
*/
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
/**
* V5 flow only. FileReviewer has completed the claim and the owner has signed;
* the FileMaker who created the file must now approve before fanavaran submission.
*/
WAITING_FOR_FILE_MAKER_APPROVAL = "WAITING_FOR_FILE_MAKER_APPROVAL",
/**
* V5 flow only. FileMaker rejected the file back to FileReviewer
* for correction (adjust pricing / back-and-forth with user and re-submit).
*/
FILE_MAKER_REJECTED = "FILE_MAKER_REJECTED",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
AUTO_CLOSED = "AUTO_CLOSED",
STOPPED = "STOPPED"
}

View File

@@ -0,0 +1,97 @@
import { ResendItemType } from "./resendItemType.enum";
const RESEND_ITEM_VALUES = new Set<string>(Object.values(ResendItemType));
/**
* Map multipart / client field names and DB typos to canonical {@link ResendItemType} values.
*/
export function normalizeResendRequestedItemKey(raw: string): string | null {
const t = String(raw ?? "").trim();
if (!t) return null;
if (RESEND_ITEM_VALUES.has(t)) return t;
const lower = t.toLowerCase();
for (const v of RESEND_ITEM_VALUES) {
if (v.toLowerCase() === lower) return v;
}
return null;
}
/** Deduplicated list of valid requested item keys. */
export function normalizeResendRequestedItemsList(
items: string[] | undefined | null,
): string[] {
const out: string[] = [];
const seen = new Set<string>();
for (const raw of items || []) {
const c = normalizeResendRequestedItemKey(String(raw));
if (c && !seen.has(c)) {
seen.add(c);
out.push(c);
}
}
return out;
}
/**
* Clone `uploadedDocuments` from a Mongoose subdoc (plain object or Map) into a plain object
* so merges and {@link isResendPartyItemSatisfied} see existing keys.
*/
export function cloneResendUploadedDocuments(
raw: unknown,
): Record<string, unknown> {
if (raw == null || typeof raw !== "object") return {};
if (raw instanceof Map) {
const o: Record<string, unknown> = {};
for (const [k, v] of raw.entries()) {
o[String(k)] = v;
}
return o;
}
return { ...(raw as Record<string, unknown>) };
}
/** How the mobile/web client should collect each resend item (no workflow-step manager). */
export type ResendItemInputKind =
| "document_camera"
| "voice"
| "video"
| "text";
export function getResendItemInputKind(item: string): ResendItemInputKind {
if (item === ResendItemType.VOICE) return "voice";
if (item === ResendItemType.DESCRIPTION) return "text";
return "document_camera";
}
export function buildResendItemsWithUi(requestedItems: string[]) {
const normalized = normalizeResendRequestedItemsList(requestedItems);
return normalized.map((item) => ({
item,
inputKind: getResendItemInputKind(item),
}));
}
/** Whether a single requested item is satisfied on a party's resend row (after merges). */
export function isResendPartyItemSatisfied(
item: string,
row: {
uploadedDocuments?: Record<string, unknown>;
resendVoiceId?: unknown;
resendVideoId?: unknown;
userTextDescription?: string;
},
): boolean {
const uploaded = row.uploadedDocuments || {};
if (item === ResendItemType.DESCRIPTION) {
return !!(
row.userTextDescription && String(row.userTextDescription).trim()
);
}
if (item === ResendItemType.VOICE) {
return !!row.resendVoiceId;
}
// if (item === ResendItemType.VIDEO) {
// return !!row.resendVideoId;
// }
return !!uploaded[item];
}

View File

@@ -5,8 +5,11 @@ export enum ResendItemType {
CAR_CERTIFICATE = "carCertificate",
DRIVING_LICENSE = "drivingLicense",
CAR_GREEN_CARD = "carGreenCard",
CAR_PLATE = "carPlate",
CHASSIS_NUMBER = "chassisNumber",
// Media evidence
VOICE = "voice",
VIDEO = "video",
/** Written / text party description (maps to FIRST_DESCRIPTION / SECOND_DESCRIPTION workflow steps) */
DESCRIPTION = "description",
}

View File

@@ -10,12 +10,51 @@ export enum ClaimCaseStatus {
// User flow - damage capture
CAPTURING_PART_DAMAGES = "CAPTURING_PART_DAMAGES",
/** Damage expert asked for more documents and/or part photos; owner must upload before the case returns to the expert queue. */
WAITING_FOR_USER_RESEND = "WAITING_FOR_USER_RESEND",
// Expert flow
WAITING_FOR_DAMAGE_EXPERT = "WAITING_FOR_DAMAGE_EXPERT",
EXPERT_REVIEWING = "EXPERT_REVIEWING",
/**
* @deprecated Prefer specific post-expert statuses below. Kept for existing DB rows and reads.
* Historically used for all owner/insurer steps after expert pricing.
*/
WAITING_FOR_INSURER_APPROVAL = "WAITING_FOR_INSURER_APPROVAL",
/** Expert reply has only priced lines (`factorNeeded=false` everywhere). Owner final accept/reject at `INSURER_REVIEW`. Also set after expert finishes repair-factor validation when the case returns to final owner sign-off. */
INSURER_REVIEW_AWAITING_OWNER_SIGN = "INSURER_REVIEW_AWAITING_OWNER_SIGN",
/** Expert reply mixes priced lines and factor-needed lines: owner signs priced lines, then uploads factors (status stays through both sub-steps). */
INSURER_REVIEW_MIXED_FACTORS_PENDING = "INSURER_REVIEW_MIXED_FACTORS_PENDING",
/** Expert reply requires a repair-factor file for every line before cost validation. */
OWNER_REPAIR_FACTOR_UPLOAD_PENDING = "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
/** All required factor files are uploaded; damage expert validates factors (`UNDER_REVIEW` @ `EXPERT_COST_EVALUATION`). */
EXPERT_VALIDATING_REPAIR_FACTORS = "EXPERT_VALIDATING_REPAIR_FACTORS",
/**
* V4 split flow only. FileMaker has uploaded all initial required documents;
* the file is sealed and waiting for a FileReviewer to pick it up (accident fields,
* capture, and final blame video). Transitions to SELECTING_OUTER_PARTS when the
* FileReviewer calls select-outer-parts after submitting accident fields.
*/
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
/**
* V5 split flow only. The claim is fully evaluated and owner has signed;
* the FileMaker who created the file must approve before fanavaran submission.
*/
WAITING_FOR_FILE_MAKER_APPROVAL = "WAITING_FOR_FILE_MAKER_APPROVAL",
/**
* V5 split flow only. FileMaker rejected the completed claim back to FileReviewer
* for correction (adjust pricing, re-do expert review, back-and-forth with user).
*/
FILE_MAKER_REJECTED = "FILE_MAKER_REJECTED",
// Final states
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",

View File

@@ -6,20 +6,28 @@ export enum ClaimWorkflowStep {
// User: Damage selection phase
SELECT_OUTER_PARTS = "SELECT_OUTER_PARTS",
SELECT_OTHER_PARTS = "SELECT_OTHER_PARTS",
// User: Documentation phase
UPLOAD_REQUIRED_DOCUMENTS = "UPLOAD_REQUIRED_DOCUMENTS",
// User: Damage capture phase (per part)
// User: Damage capture phase (angles + per-part photos) — before document upload in v2
CAPTURE_PART_DAMAGES = "CAPTURE_PART_DAMAGES",
// User: Documentation phase — after captures in v2
UPLOAD_REQUIRED_DOCUMENTS = "UPLOAD_REQUIRED_DOCUMENTS",
// User submission complete
USER_SUBMISSION_COMPLETE = "USER_SUBMISSION_COMPLETE",
/** Owner fulfilling damage expert resend (extra documents and/or part images). */
USER_EXPERT_RESEND = "USER_EXPERT_RESEND",
// Expert: Damage assessment
EXPERT_DAMAGE_ASSESSMENT = "EXPERT_DAMAGE_ASSESSMENT",
/** After user objection: damage expert’s last priced reply (stored in evaluation.damageExpertReplyFinal) */
EXPERT_FINAL_REPLY = "EXPERT_FINAL_REPLY",
EXPERT_COST_EVALUATION = "EXPERT_COST_EVALUATION",
/** Owner must upload repair factor files for factorNeeded lines (before expert COST_EVALUATION). */
OWNER_UPLOAD_FACTOR_DOCUMENTS = "OWNER_UPLOAD_FACTOR_DOCUMENTS",
// Insurer approval
INSURER_REVIEW = "INSURER_REVIEW",

View File

@@ -3,4 +3,6 @@ export enum InPersonDocumentsEnum {
CarCertificate = "carCertificate",
DrivingLicense = "drivingLicense",
CarGreenCard = "carGreenCard",
}
Plate = "plate",
CarPlate = "carPlate",
}

View File

@@ -1,6 +1,9 @@
export enum ClaimRequiredDocumentType {
// Car green card
CAR_GREEN_CARD = "car_green_card",
CAR_CERTIFICATE = "car_certificate",
/** National ID card (or similar); may be requested on resend even if not in the initial upload set. */
NATIONAL_CARD = "national_card",
// Damaged party documents
DAMAGED_DRIVING_LICENSE_BACK = "damaged_driving_license_back",
@@ -17,6 +20,12 @@ export enum ClaimRequiredDocumentType {
GUILTY_CAR_CARD_FRONT = "guilty_car_card_front",
GUILTY_CAR_CARD_BACK = "guilty_car_card_back",
GUILTY_METAL_PLATE = "guilty_metal_plate",
/**
* V4/V5 only — a photo of the guilty car's damaged area, captured by the
* FileReviewer during the CAPTURE_PART_DAMAGES phase (after all car angles).
*/
GUILTY_DAMAGE_AREA = "guilty_damage_area",
}
export enum CarAngle {

View File

@@ -1,4 +1,4 @@
export enum TypeOfDamage {
Repair = "repair",
Change = "change",
Repair = "تعمیر",
Change = "تعویض",
}

View File

@@ -2,7 +2,12 @@ export enum RoleEnum {
EXPERT = "expert",
DAMAGE_EXPERT = "damage_expert",
FIELD_EXPERT = "field_expert",
REGISTRAR = "registrar",
COMPANY = "company",
ADMIN = "admin",
USER = "user",
FILE_MAKER = "file_maker",
FILE_REVIEWER = "file_reviewer",
SUPER_ADMIN = "super_admin",
CALL_CENTER = "call_center",
}

View File

@@ -1,9 +1,8 @@
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { AiService } from "./ai.service";
@Module({
imports: [HttpModule],
imports: [],
providers: [AiService],
exports: [AiService],
})

View File

@@ -4,15 +4,13 @@ import {
HttpException,
HttpStatus,
Injectable,
Logger,
OnModuleInit,
} from "@nestjs/common";
import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data";
import { ConfigService } from "@nestjs/config";
import { AxiosRequestConfig } from "axios"; // TODO: Change all axios usages to HttpModule
@Injectable()
export class AiService implements OnModuleInit {
private readonly logger = new Logger(AiService.name);
private apiKey: string;
private accessToken: string = null;
@@ -20,18 +18,20 @@ export class AiService implements OnModuleInit {
private readonly loginOptions: AxiosRequestConfig = {
method: "POST",
headers: { "Content-Type": "application/json" },
url: `${process.env.AI_URL_V2}/auth/login`,
url: `${this.configService.get<string>("AI_URL_V2")}/auth/login`,
data: {
username: process.env.AI_USERNAME,
password: process.env.AI_PASSWORD,
username: this.configService.get<string>("AI_USERNAME"),
password: this.configService.get<string>("AI_PASSWORD"),
},
timeout: 30000, // 30 second timeout
timeout: 1000, // TODO: Make this ENV
};
constructor(private readonly configService: ConfigService) {}
private get profileOptions(): AxiosRequestConfig {
return {
method: "GET",
url: `${process.env.AI_URL_V2}/auth/profile`,
url: `${this.configService.get<string>("AI_URL_V2")}/auth/profile`,
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
@@ -50,28 +50,16 @@ export class AiService implements OnModuleInit {
};
}
constructor() {}
async onModuleInit() {
try {
const res = await this.login();
if (res?.accessToken) {
this.logger.verbose("AI Service Authenticated Successfully.");
this.accessToken = res.accessToken;
await this.getApiKey();
this.logger.log("AI Service initialized and ready.");
} else {
this.logger.warn(
"AI Service Unavailable: Login did not return an access token. Will retry on first request.",
);
}
// if (res?.accessToken) {
// this.accessToken = res.accessToken;
// await this.getApiKey();
// }
} catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called
this.logger.warn(
"AI Service Unavailable: Failed during initial login. Will retry on first request.",
);
this.logger.warn(`Error: ${error.message}`);
// Reset tokens so re-authentication will be attempted
this.accessToken = null;
this.apiKey = null;
@@ -79,55 +67,34 @@ export class AiService implements OnModuleInit {
}
private async login() {
try {
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
} catch (err) {
const errorMessage = err.response?.data?.message || err.message || "Unknown error";
const statusCode = err.response?.status || 500;
this.logger.error(`AI login failed: ${errorMessage} (Status: ${statusCode})`);
if (err.response?.data) {
this.logger.error(`AI login error details: ${JSON.stringify(err.response.data, null, 2)}`);
}
throw new HttpException(
`Could not authenticate with AI service: ${errorMessage}`,
statusCode >= 400 && statusCode < 500 ? statusCode : HttpStatus.UNAUTHORIZED,
);
}
// const loginResponse = await axios.request(this.loginOptions);
// return loginResponse.data;
}
private async getApiKey() {
try {
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
this.logger.log("Successfully retrieved AI gateway API key.");
return this.apiKey;
} catch (err) {
this.logger.error("Failed to retrieve AI API key:", err.message);
throw new HttpException(
"Could not get API key from AI service",
HttpStatus.FAILED_DEPENDENCY,
);
}
// const profileResponse = await axios.request(this.profileOptions);
// this.apiKey = profileResponse.data.apiKey.key;
// return this.apiKey;
}
public async aiRequestImage(file: { path: string; fileName?: string }): Promise<any> {
public async aiRequestImage(file: {
path: string;
fileName?: string;
}): Promise<any> {
// Ensure authentication is set up
if (!this.accessToken || !this.apiKey) {
this.logger.warn("AI service not authenticated, attempting to re-authenticate...");
try {
const res = await this.login();
if (res?.accessToken) {
this.accessToken = res.accessToken;
await this.getApiKey();
} else {
throw new HttpException(
"AI Service authentication failed",
HttpStatus.UNAUTHORIZED,
);
}
// if (res?.accessToken) {
// this.accessToken = res.accessToken;
// await this.getApiKey();
// } else {
// throw new HttpException(
// "AI Service authentication failed",
// HttpStatus.UNAUTHORIZED,
// );
// }
} catch (error) {
this.logger.error("Failed to re-authenticate AI service:", error.message);
throw new HttpException(
"AI Service authentication failed",
HttpStatus.UNAUTHORIZED,
@@ -136,114 +103,86 @@ export class AiService implements OnModuleInit {
}
// Resolve relative paths to absolute paths
const filePath = file.path.startsWith("/")
? file.path
const filePath = file.path.startsWith("/")
? file.path
: join(process.cwd(), file.path.replace(/^\.\//, ""));
this.logger.log(`Processing AI image request for: ${filePath}`);
// Check if file exists
if (!existsSync(filePath)) {
this.logger.error(`File not found at path: ${filePath}`);
throw new HttpException(
`File not found: ${file.path}`,
HttpStatus.NOT_FOUND,
);
}
const form = new FormData();
// const form = new FormData();
const fileStream = createReadStream(filePath);
// Append file with filename if available
if (file.fileName) {
form.append("images", fileStream, file.fileName);
// form.append("images", fileStream, file.fileName);
} else {
// Extract filename from path if not provided
const pathParts = filePath.split("/");
const extractedFileName = pathParts[pathParts.length - 1];
form.append("images", fileStream, extractedFileName);
// form.append("images", fileStream, extractedFileName);
}
try {
const requestHeaders = {
...this.imageProcessOptions.headers,
...form.getHeaders(),
// ...form.getHeaders(),
};
this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`);
this.logger.log(`[STEP 1/4] File: ${filePath}, Filename: ${file.fileName || 'extracted from path'}`);
this.logger.log(`[STEP 1/4] FormData Content-Type: ${form.getHeaders()['content-type']}`);
this.logger.log(`[STEP 1/4] Authorization header present: ${!!requestHeaders.Authorization}`);
this.logger.log(`[STEP 1/4] Gateway API key present: ${!!this.apiKey}`);
this.logger.log(`[STEP 1/4] Request method: POST`);
this.logger.log(`[STEP 1/4] FormData field name: "images"`);
// Get file stats for debugging
const fs = require('fs');
const fs = require("fs");
const stats = fs.statSync(filePath);
this.logger.log(`[STEP 1/4] File size: ${stats.size} bytes`);
this.logger.log(`[STEP 1/4] File exists: ${existsSync(filePath)}`);
const response = await axios.request({
...this.imageProcessOptions,
headers: requestHeaders,
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`);
// const response = await axios.request({
// ...this.imageProcessOptions,
// headers: requestHeaders,
// // data: form,
// maxContentLength: Infinity,
// maxBodyLength: Infinity,
// });
// Validate response structure
if (!response.data) {
this.logger.error(`[ERROR] AI response is empty or missing data`);
throw new HttpException(
"AI Service returned empty response",
HttpStatus.BAD_GATEWAY,
);
}
// if (!response.data) {
// throw new HttpException(
// "AI Service returned empty response",
// HttpStatus.BAD_GATEWAY,
// );
// }
// Check for error in response first (AI service returns 201 with error in body)
if (response.data.error) {
this.logger.error(`[ERROR] AI service returned an error in response body`);
this.logger.error(`[ERROR] Error message: ${response.data.error}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
`AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY,
);
}
// // Check for error in response first (AI service returns 201 with error in body)
// if (response.data.error) {
// throw new HttpException(
// `AI Service error: ${response.data.error}`,
// HttpStatus.BAD_GATEWAY,
// );
// }
// Check for processed image (downloadLink)
if (!response.data.downloadLink) {
this.logger.error(`[ERROR] AI response missing processed image (downloadLink)`);
this.logger.error(`[ERROR] Response structure: ${JSON.stringify(Object.keys(response.data))}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
"AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY,
);
}
// // Check for processed image (downloadLink)
// if (!response.data.downloadLink) {
// throw new HttpException(
// "AI Service did not return processed image (downloadLink missing)",
// HttpStatus.BAD_GATEWAY,
// );
// }
// Check for reports
if (!response.data.reports) {
this.logger.warn(`[WARNING] AI response missing reports object, but downloadLink exists`);
this.logger.warn(`[WARNING] Response keys: ${JSON.stringify(Object.keys(response.data))}`);
}
this.logger.log(`[STEP 3/4] Validated AI response - downloadLink: ${response.data.downloadLink ? 'present' : 'missing'}, reports: ${response.data.reports ? 'present' : 'missing'}`);
return response.data;
// return response.data;
} catch (er) {
// Determine error source
let errorSource = "UNKNOWN";
let errorMessage = er.message;
let errorDetails = "No error details available";
if (er.response) {
errorSource = "AI_SERVICE_RESPONSE";
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`;
errorDetails = er.response?.data
errorMessage =
er.response?.data?.message ||
er.message ||
`HTTP ${er.response.status}`;
errorDetails = er.response?.data
? JSON.stringify(er.response.data, null, 2)
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
} else if (er.request) {
@@ -254,15 +193,7 @@ export class AiService implements OnModuleInit {
errorSource = "REQUEST_SETUP_ERROR";
errorMessage = er.message || "Error setting up request";
}
this.logger.error(`[ERROR] AI request failed - Source: ${errorSource}`);
this.logger.error(`[ERROR] File path: ${filePath}`);
this.logger.error(`[ERROR] Error message: ${errorMessage}`);
this.logger.error(`[ERROR] Error details: ${errorDetails}`);
if (er.stack) {
this.logger.error(`[ERROR] Stack trace: ${er.stack}`);
}
// Re-throw with detailed error information
throw new HttpException(
`[${errorSource}] ${errorMessage}`,

View File

@@ -1,51 +1,54 @@
import { join } from "node:path";
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { HttpModule } from "@nestjs/axios";
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
import { MongooseModule } from "@nestjs/mongoose";
import { ScheduleModule } from "@nestjs/schedule";
import { ServeStaticModule } from "@nestjs/serve-static";
import * as dotenv from "dotenv";
import { CommandModule } from "nestjs-command";
import { AiModule } from "./ai/ai.module";
import { AuthModule } from "./auth/auth.module";
import { ClaimRequestManagementModule } from "./claim-request-management/claim-request-management.module";
import { ClientModule } from "./client/client.module";
import { ExpertBlameModule } from "./expert-blame/expert-blame.module";
import { FanavaranModule } from "./fanavaran/fanavaran.module";
import { ExpertClaimModule } from "./expert-claim/expert-claim.module";
import { ExpertInsurerModule } from "./expert-insurer/expert-insurer.module";
import { CaseExpertReportModule } from "./case-expert-report/case-expert-report.module";
import { LookupsModule } from "./lookups/lookups.module";
import { PlatesModule } from "./plates/plates.module";
import { ProfileModule } from "./profile/profile.module";
import { SandHubModule } from "./sand-hub/sand-hub.module";
import { SystemSettingsModule } from "./system-settings/system-settings.module";
import { ReportsModule } from "./reports/reports.module";
import { RequestManagementModule } from "./request-management/request-management.module";
import { UsersModule } from "./users/users.module";
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
import { CronModule } from "./utils/cron/cron.module";
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
dotenv.config();
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
import { DatabaseModule } from "./core/database/database.module";
import { AppConfigModule } from "./core/config/config.module";
import { SuperAdminModule } from "./super-admin/super-admin.module";
import { createHttpModuleOptions } from "./core/config/http-proxy.factory";
@Module({
imports: [
CommandModule,
ScheduleModule.forRoot(),
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
AppConfigModule,
DatabaseModule,
CronModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, "..", "files"),
// process.cwd() is always the project/container root (/app in Docker),
// so the volume-mounted /app/files is resolved correctly regardless of
// where the compiled dist files live (__dirname would resolve to
// /app/dist/src because TypeScript preserves the src/ prefix in outDir).
rootPath: join(process.cwd(), "files"),
serveRoot: "/files",
}),
MongooseModule.forRoot(
`mongodb://${process.env.MONGO_URL}:${process.env.MONGO_PORT}/`,
{
dbName: "yara724",
autoIndex: true,
user: process.env.MONGO_USER,
pass: process.env.MONGO_PASS,
authMechanism: "SCRAM-SHA-256",
tls: true,
tlsAllowInvalidCertificates: true,
},
),
UsersModule,
AuthModule,
ClientModule,
@@ -53,16 +56,33 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
PlatesModule,
RequestManagementModule,
SandHubModule,
SystemSettingsModule,
ExpertBlameModule,
ClaimRequestManagementModule,
FanavaranModule,
ExpertClaimModule,
CaseExpertReportModule,
AiModule,
ReportsModule,
ExpertInsurerModule,
LookupsModule,
WorkflowStepManagementModule,
SuperAdminModule,
],
controllers: [],
providers: [],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: UnicodeDigitsNormalizeInterceptor,
},
// {
// provide: APP_PIPE,
// useValue: new ValidationPipe({
// transform: true,
// whitelist: true,
// forbidNonWhitelisted: false,
// }),
// },
],
})
export class AppModule {}

View File

@@ -5,17 +5,24 @@ import {
Param,
Patch,
Post,
Query,
Req,
Res,
UseGuards,
} from "@nestjs/common";
import type { Response } from "express";
import {
ApiBody,
ApiAcceptedResponse,
ApiOperation,
ApiResponse,
ApiTags,
ApiBearerAuth,
} from "@nestjs/swagger";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { CaptchaChallengeService } from "src/captcha/captcha-challenge.service";
import { CaptchaResponseDto } from "src/auth/dto/captcha-response.dto";
import { GetCaptchaImageQueryDto } from "src/auth/dto/get-captcha-image-query.dto";
import {
ForgetPasswordSendCodeDto,
ForgetPasswordVerifyCodeDto,
@@ -23,57 +30,198 @@ import {
import { LoginActorDto } from "src/auth/dto/actor/login.actor.dto";
import { ActorEditUserProfileDto } from "src/auth/dto/actor/profile.actor.dto";
import { CreateFieldExpertDto } from "src/auth/dto/actor/create-field-expert.actor.dto";
import { CreateRegistrarDto } from "src/auth/dto/actor/create-registrar.actor.dto";
import {
GenuineRegisterDto,
InsurerRegisterDto,
LegalRegisterDto,
} from "src/auth/dto/actor/register.actor.dto";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { SuperAdminGuard } from "src/super-admin/guards/super-admin.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
@Controller("actor")
@ApiTags("actor")
export class ActorAuthController {
constructor(private readonly actorAuthService: ActorAuthService) {}
constructor(
private readonly actorAuthService: ActorAuthService,
private readonly captchaChallengeService: CaptchaChallengeService,
) {}
@Get("captcha")
@ApiOperation({
summary: "Get a login captcha",
description:
"Issues a new captcha challenge. Returns `captchaId`, `image`, and `expiresAt`. " +
"Send `captchaId` and the typed characters as `captcha` on POST /actor/login.\n\n" +
"Optional `format=raw` returns image/svg+xml for browser preview (same captchaId is in JSON when omitted).",
})
@ApiResponse({ status: 200, type: CaptchaResponseDto })
async getCaptcha(
@Query() query: GetCaptchaImageQueryDto,
@Res({ passthrough: true }) res: Response,
) {
const result = await this.captchaChallengeService.issue();
if (query.format === "raw") {
const svg = await this.captchaChallengeService.getImageById(
result.captchaId,
);
res.setHeader("X-Captcha-Id", result.captchaId);
res.type("image/svg+xml");
res.send(svg);
return;
}
return result;
}
@Get("captcha/:captchaId/image")
@ApiOperation({
summary: "View captcha image by id",
description: "Returns raw SVG for a previously issued captcha challenge.",
})
async getCaptchaImage(
@Param("captchaId") captchaId: string,
@Res({ passthrough: true }) res: Response,
) {
const svg = await this.captchaChallengeService.getImageById(captchaId);
res.type("image/svg+xml");
res.send(svg);
}
/**
* @deprecated Use the unified actor onboarding flow instead. This endpoint
* will be removed in a future release.
*/
@Post("register/genuine")
@UseGuards(SuperAdminGuard)
@ApiOperation({
deprecated: true,
summary: "[DEPRECATED] Genuine actor registration",
description:
"Deprecated — kept only for legacy clients. Use the unified actor onboarding flow instead. Will be removed.",
})
@ApiBody({ type: GenuineRegisterDto })
async registerGenuine(@Body() body: GenuineRegisterDto) {
return await this.actorAuthService.genuineRegister(body);
}
/**
* @deprecated Use the unified actor onboarding flow instead. This endpoint
* will be removed in a future release.
*/
@Post("register/legal")
@UseGuards(SuperAdminGuard)
@ApiOperation({
deprecated: true,
summary: "[DEPRECATED] Legal actor registration",
description:
"Deprecated — kept only for legacy clients. Use the unified actor onboarding flow instead. Will be removed.",
})
@ApiBody({ type: LegalRegisterDto })
async registerLegal(@Body() body: LegalRegisterDto) {
return await this.actorAuthService.legalRegister(body);
}
@Post("register/insurer")
@UseGuards(SuperAdminGuard)
@ApiBody({ type: InsurerRegisterDto })
async registerInsurer(@Body() body: InsurerRegisterDto) {
return await this.actorAuthService.insurerRegister(body);
}
/** Mock: create a field expert for testing. Make private later. */
/** Requires super-admin token. */
@Post("create-field-expert")
@UseGuards(SuperAdminGuard)
@ApiBody({ type: CreateFieldExpertDto })
@ApiAcceptedResponse()
async createFieldExpert(@Body() body: CreateFieldExpertDto) {
return await this.actorAuthService.createFieldExpertMock(body);
}
/** Requires super-admin token. */
@Post("create-registrar")
@UseGuards(SuperAdminGuard)
@ApiBody({ type: CreateRegistrarDto })
@ApiAcceptedResponse()
async createRegistrar(@Body() body: CreateRegistrarDto) {
return await this.actorAuthService.createRegistrarMock(body);
}
@UseGuards(LocalActorAuthGuard)
@Post("login")
@Roles()
@ApiOperation({
summary: "Actor login (returns access + refresh tokens)",
description:
'Authenticate any non-end-user actor (insurer/company, blame expert, damage expert, registrar, field expert, admin). Submit `role` as an array — e.g. `["damage_expert"]` — together with password and one of `username` / `email` / `nationalCode`. On success the response contains the JWT pair and the resolved profile.',
})
@ApiBody({
type: LoginActorDto,
description: "user verify otp -- call this api and get a tokens",
description:
"Login payload. Pick one of the swagger examples below to see the exact body shape per role.",
examples: {
company: {
summary: "Insurer / company portal",
description:
"Sample tenant credentials for the insurer (company) panel.",
value: {
role: "company",
username: "saman_insurer@gmail.com",
password: "123321",
captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
expert: {
summary: "Blame expert panel",
description: "Sample credentials for a blame (`expert`) account.",
value: {
role: "expert",
username: "blame@gmail.com",
password: "123321",
captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
damage_expert: {
summary: "Damage expert (claim) panel",
description: "Sample credentials for a damage-expert account.",
value: {
role: "damage_expert",
username: "claim@gmail.com",
password: "123321",
captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
field_expert: {
summary: "Field expert panel",
description:
"Login with email+password or nationalCode+password for seeded Parsian field experts.",
value: {
role: "field_expert",
nationalCode: "0051967839",
password: "Parsian@724",
captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
},
})
@ApiResponse({
status: 404,
description: "No actor account exists for the given email and role",
})
@ApiResponse({
status: 401,
description: "Wrong password or role mismatch",
})
@ApiAcceptedResponse()
async login(@Body() body, @Req() req, @ClientKey() client) {
return await this.actorAuthService.loginActors(req.user);
async login(@Req() req: { user: Record<string, unknown> }) {
return req.user;
}
@Post("forget-password")

View File

@@ -22,11 +22,15 @@ export class UserAuthController {
@Post("/send-otp")
@ApiBody({
type: UserLoginDto,
description: "user login api -- call this api and send otp",
description: "Users can ask for OTP via this API and receive it",
})
@ApiAcceptedResponse()
async sendOtpRq(@Body() body: UserLoginDto) {
const res = await this.userAuthService.sendOtpRequest(body.mobile);
const res = await this.userAuthService.sendOtpRequest(body.mobile, {
linkToken: body.linkToken,
linkContext: body.linkContext,
});
if (res) {
throw new HttpException(res, HttpStatus.ACCEPTED);
}
@@ -36,7 +40,8 @@ export class UserAuthController {
@UseGuards(LocalUserAuthGuard)
@ApiBody({
type: UserVerifyOtp,
description: "user verify otp -- call this api and get a tokens",
description:
"Users can send their credentials and get their access token to server",
})
@ApiAcceptedResponse()
async login(@Body() body, @Req() req, @CurrentUser() user) {

View File

@@ -18,6 +18,7 @@ import {
RegisterDtoRs,
} from "src/auth/dto/actor/register.actor.dto";
import { StateListDtoRs } from "src/auth/dto/actor/states.dto";
import { CaptchaChallengeService } from "src/captcha/captcha-challenge.service";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
@@ -25,9 +26,13 @@ import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-ex
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
import { CallCenterAgentDbService } from "src/users/entities/db-service/call-center-agent.db.service";
import { HashService } from "src/utils/hash/hash.service";
// import { MailService } from "src/utils/mail/mail.service";
import { OtpService } from "src/utils/otp/otp.service";
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
import { SuperAdminDbService } from "src/super-admin/entities/db-service/super-admin.db.service";
function pick(obj: Record<string, any>, keys: string[]) {
const out: Record<string, any> = {};
@@ -46,10 +51,15 @@ export class ActorAuthService {
private readonly expertDbService: ExpertDbService,
private readonly damageExpertDbService: DamageExpertDbService,
private readonly fieldExpertDbService: FieldExpertDbService,
private readonly registrarDbService: RegistrarDbService,
private readonly insurerExpertDbService: InsurerExpertDbService,
// private readonly mailService: MailService, // Mailer disabled – not used
private readonly clientDbService: ClientDbService,
private readonly otpService: OtpService,
private readonly otpService: OtpGeneratorService,
private readonly captchaChallengeService: CaptchaChallengeService,
private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService,
private readonly superAdminDbService: SuperAdminDbService,
private readonly callCenterAgentDbService: CallCenterAgentDbService,
) {}
// TODO convrt to class for dynamic controller
@@ -61,7 +71,7 @@ export class ActorAuthService {
res = await this.expertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.expertDbService.findOne({ email: username });
else res = await this.findActorByLoginIdentifier(this.expertDbService, username);
break;
case RoleEnum.DAMAGE_EXPERT:
if (username == null && userId)
@@ -69,7 +79,10 @@ export class ActorAuthService {
_id: new Types.ObjectId(userId),
});
else
res = await this.damageExpertDbService.findOne({ email: username });
res = await this.findActorByLoginIdentifier(
this.damageExpertDbService,
username,
);
break;
case RoleEnum.FIELD_EXPERT:
if (username == null && userId)
@@ -77,59 +90,162 @@ export class ActorAuthService {
_id: new Types.ObjectId(userId),
});
else
res = await this.fieldExpertDbService.findOne({ email: username });
res = await this.fieldExpertDbService.findByLoginIdentifier(username);
break;
case RoleEnum.REGISTRAR:
if (username == null && userId)
res = await this.registrarDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.registrarDbService.findOne({ email: username });
break;
case RoleEnum.COMPANY:
res = await this.insurerExpertDbService.findOne({ email: username });
break;
case RoleEnum.FILE_MAKER:
if (username == null && userId)
res = await this.fileMakerDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.fileMakerDbService.findByLoginIdentifier(username);
break;
case RoleEnum.FILE_REVIEWER:
if (username == null && userId)
res = await this.fileReviewerDbService.findOne({
_id: new Types.ObjectId(userId),
});
else
res =
await this.fileReviewerDbService.findByLoginIdentifier(username);
break;
case RoleEnum.SUPER_ADMIN:
if (username == null && userId)
res = await this.superAdminDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.superAdminDbService.findByLoginIdentifier(username);
break;
case RoleEnum.CALL_CENTER:
if (username == null && userId)
res = await this.callCenterAgentDbService.findOne({
_id: new Types.ObjectId(userId),
});
else
res = await this.callCenterAgentDbService.findByLoginIdentifier(username);
break;
default:
return null;
}
return res;
}
async validateActor(username: string, pass: string, role): Promise<any> {
const user = await this.dynamicDbController(role, username);
if (user) {
if (user.role !== role) {
throw new UnauthorizedException("user not assigned to this role");
}
if (!(await this.hashService.compare(pass, user.password))) {
throw new UnauthorizedException(
"password is incorrect or access Denied",
);
} else {
return user;
}
/** Normalizes `role` from login body (string or single-element array). */
parseActorLoginRole(role: unknown): RoleEnum {
const raw = Array.isArray(role) ? role[0] : role;
if (
typeof raw !== "string" ||
!Object.values(RoleEnum).includes(raw as RoleEnum)
) {
throw new BadRequestException(
`Invalid role. Expected one of: ${Object.values(RoleEnum).join(", ")}`,
);
}
return null;
return raw as RoleEnum;
}
async loginActors(user: any) {
let foundedUser = await this.dynamicDbController(user.role, user.username);
if (foundedUser) {
const payload = {
username: foundedUser.username || foundedUser.email,
sub: foundedUser._id,
fullName:
`${foundedUser.firstName || ""} ${foundedUser.lastName || ""}`.trim(),
role: foundedUser.role || "User",
userType: foundedUser.userType || "UserType",
clientKey: foundedUser.clientKey || null,
};
const accToken = this.jwtService.sign(payload, {
secret: `${process.env.SECRET}`,
expiresIn: "1h",
});
return {
...payload,
access_token: accToken,
};
} else {
throw new UnauthorizedException("expert or damage_expert not found");
parseActorLoginUsername(body: Record<string, unknown>): string {
const username = body?.username ?? body?.email ?? body?.nationalCode;
if (typeof username !== "string" || !username.trim()) {
throw new BadRequestException(
"username, email, or nationalCode is required",
);
}
return username.trim();
}
private async findActorByLoginIdentifier(
dbService: { findOne: (filter: any) => Promise<any> },
identifier: string,
) {
const id = identifier.trim();
const or: Record<string, string>[] = [{ email: id }, { username: id }];
if (/^\d{10}$/.test(id)) {
or.push({ nationalCode: id });
}
return dbService.findOne({ $or: or });
}
issueActorTokens(actor: {
_id: Types.ObjectId;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
role?: string;
userType?: string;
clientKey?: Types.ObjectId | string | null;
}) {
const payload = {
username:
actor.username || actor.email || (actor as any).nationalCode || null,
sub: actor._id,
fullName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
role: actor.role || "User",
userType: actor.userType || "UserType",
clientKey: actor.clientKey ? String(actor.clientKey) : null,
};
const access_token = this.jwtService.sign(payload, {
secret: `${process.env.JWT_SECRET}`,
expiresIn: "1h",
});
return { ...payload, access_token };
}
async validateActor(
username: string,
pass: string,
role: RoleEnum,
): Promise<any> {
const user = await this.dynamicDbController(role, username);
if (!user) {
throw new NotFoundException("حساب کاربری یافت نشد");
}
if (user.role !== role) {
throw new UnauthorizedException("این حساب به نقش انتخاب‌شده تعلق ندارد");
}
if (!(await this.hashService.compare(pass, user.password))) {
throw new UnauthorizedException("نام کاربری یا رمز عبور اشتباه است");
}
return user;
}
/**
* Authenticates an actor from a login request body (role, username/email, password).
*/
async loginFromCredentials(body: Record<string, unknown>) {
const role = this.parseActorLoginRole(body?.role);
const username = this.parseActorLoginUsername(body);
const password = body?.password;
if (typeof password !== "string" || !password) {
throw new BadRequestException("رمز عبور الزامی است");
}
const captchaId =
typeof body?.captchaId === "string" ? body.captchaId : undefined;
const captcha =
typeof body?.captcha === "string" ? body.captcha : undefined;
await this.captchaChallengeService.verify(captchaId, captcha);
const actor = await this.validateActor(username, password, role);
return this.issueActorTokens(actor);
}
/** @deprecated Prefer {@link loginFromCredentials}. Kept for internal callers. */
async loginActors(user: any) {
if (user?.access_token && user?.sub) {
return user;
}
return this.loginFromCredentials(user as Record<string, unknown>);
}
async registerActors(
@@ -164,7 +280,7 @@ export class ActorAuthService {
firstName: body.firstName,
lastName: body.lastName,
phone: body.phone,
mobile:body.mobile,
mobile: body.mobile,
city: body.city,
state: body.state,
address: body.address,
@@ -273,6 +389,10 @@ export class ActorAuthService {
actor = await this.fieldExpertDbService.findOne(filter);
dbServiceToUpdate = this.fieldExpertDbService as any;
}
if (!actor) {
actor = await this.registrarDbService.findOne(filter);
dbServiceToUpdate = this.registrarDbService as any;
}
if (!actor) {
throw new NotFoundException("actor not found");
@@ -309,6 +429,10 @@ export class ActorAuthService {
userExist = await this.fieldExpertDbService.findOne({ email });
dbServiceToUpdate = this.fieldExpertDbService;
}
if (!userExist) {
userExist = await this.registrarDbService.findOne({ email });
dbServiceToUpdate = this.registrarDbService as any;
}
if (!userExist) throw new NotFoundException("user not found");
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
@@ -376,13 +500,8 @@ export class ActorAuthService {
"state",
"address",
],
field_expert: [
"firstName",
"lastName",
"email",
"phone",
"mobile",
],
field_expert: ["firstName", "lastName", "email", "phone", "mobile"],
registrar: ["email"],
};
const allowedFields = allowedFieldsByRole[role];
@@ -407,7 +526,11 @@ export class ActorAuthService {
}
// fetch user detail (document or plain object)
const document = await this.dynamicDbController(role, currentUser.role === "company" ? currentUser.username : null, userId);
const document = await this.dynamicDbController(
role,
currentUser.role === "company" ? currentUser.username : null,
userId,
);
if (!document) throw new NotFoundException("Profile not found");
@@ -498,4 +621,34 @@ export class ActorAuthService {
throw er;
}
}
async createRegistrarMock(body: {
email: string;
password: string;
clientId: string;
}) {
const hashPassword = await this.hashService.hash(body.password);
const payload = {
email: body.email.toLowerCase().trim(),
password: hashPassword,
clientKey: new Types.ObjectId(body.clientId),
role: RoleEnum.REGISTRAR,
};
try {
const created = await this.registrarDbService.create(payload as any);
return {
id: (created as any)._id,
email: (created as any).email,
clientKey: (created as any).clientKey,
role: (created as any).role,
};
} catch (er) {
if (er.code === 11000) {
throw new BadRequestException(
"A registrar with this email already exists.",
);
}
throw er;
}
}
}

View File

@@ -0,0 +1,38 @@
import {
BadRequestException,
UnauthorizedException,
} from "@nestjs/common";
export enum CaptchaAuthErrorCode {
CAPTCHA_REQUIRED = "CAPTCHA_REQUIRED",
CAPTCHA_NOT_FOUND = "CAPTCHA_NOT_FOUND",
CAPTCHA_EXPIRED = "CAPTCHA_EXPIRED",
CAPTCHA_INVALID = "CAPTCHA_INVALID",
}
const messages: Record<CaptchaAuthErrorCode, string> = {
[CaptchaAuthErrorCode.CAPTCHA_REQUIRED]:
"کپچا الزامی است. ابتدا تصویر کپچا را دریافت کنید.",
[CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND]:
"شناسه کپچا یافت نشد. تصویر جدیدی درخواست دهید.",
[CaptchaAuthErrorCode.CAPTCHA_EXPIRED]:
"کپچا منقضی شده است. تصویر جدیدی درخواست دهید.",
[CaptchaAuthErrorCode.CAPTCHA_INVALID]: "کپچا نامعتبر است.",
};
export function captchaAuthErrorBody(code: CaptchaAuthErrorCode) {
return {
code,
message: messages[code],
};
}
export function throwCaptchaAuthError(code: CaptchaAuthErrorCode): never {
if (
code === CaptchaAuthErrorCode.CAPTCHA_REQUIRED ||
code === CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND
) {
throw new BadRequestException(captchaAuthErrorBody(code));
}
throw new UnauthorizedException(captchaAuthErrorBody(code));
}

View File

@@ -0,0 +1,46 @@
import {
BadRequestException,
ForbiddenException,
UnauthorizedException,
} from "@nestjs/common";
export enum UserAuthErrorCode {
USER_NOT_FOUND = "USER_NOT_FOUND",
OTP_REQUIRED = "OTP_REQUIRED",
OTP_EXPIRED = "OTP_EXPIRED",
OTP_INVALID = "OTP_INVALID",
OTP_REQUEST_TOO_SOON = "OTP_REQUEST_TOO_SOON",
LINK_NOT_FOUND = "LINK_NOT_FOUND",
LINK_MOBILE_MISMATCH = "LINK_MOBILE_MISMATCH",
}
const messages: Record<UserAuthErrorCode, string> = {
[UserAuthErrorCode.USER_NOT_FOUND]: "کاربر یافت نشد",
[UserAuthErrorCode.OTP_REQUIRED]: "ابتدا درخواست کد یکبار مصرف دهید",
[UserAuthErrorCode.OTP_EXPIRED]: "کد یکبار مصرف منقضی شده است",
[UserAuthErrorCode.OTP_INVALID]: "کد یکبار مصرف نامعتبر است",
[UserAuthErrorCode.OTP_REQUEST_TOO_SOON]:
"لطفاً تا انقضای کد فعلی صبر کنید",
[UserAuthErrorCode.LINK_NOT_FOUND]: "لینک پیامکی یافت نشد",
[UserAuthErrorCode.LINK_MOBILE_MISMATCH]:
"این شماره موبایل مجاز به استفاده از این لینک نیست",
};
export function userAuthErrorBody(code: UserAuthErrorCode) {
return {
code,
message: messages[code],
};
}
export function throwUserAuthError(code: UserAuthErrorCode): never {
// if (code === UserAuthErrorCode.OTP_REQUEST_TOO_SOON) {
// throw new BadRequestException(userAuthErrorBody(code));
// }
if (code === UserAuthErrorCode.LINK_MOBILE_MISMATCH) {
throw new ForbiddenException(userAuthErrorBody(code));
}
throw new UnauthorizedException(userAuthErrorBody(code));
}

View File

@@ -0,0 +1,218 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import {
UserAuthErrorCode,
throwUserAuthError,
} from "src/auth/auth-services/user-auth-error";
import { ClaimCase } from "src/claim-request-management/entites/schema/claim-cases.schema";
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { normalizeIranMobile } from "src/helpers/iran-mobile";
import { BlameRequest } from "src/request-management/entities/schema/blame-cases.schema";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { RequestManagementModel } from "src/request-management/entities/schema/request-management.schema";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
@Injectable()
export class UserLinkAccessService {
constructor(
@InjectModel(RequestManagementModel.name)
private readonly requestManagementModel: Model<RequestManagementModel>,
@InjectModel(BlameRequest.name)
private readonly blameRequestModel: Model<BlameRequest>,
@InjectModel(ClaimRequestManagementModel.name)
private readonly claimRequestManagementModel: Model<ClaimRequestManagementModel>,
@InjectModel(ClaimCase.name)
private readonly claimCaseModel: Model<ClaimCase>,
private readonly userDbService: UserDbService,
) {}
async assertMobileAllowed(params: {
mobile: string;
linkToken?: string;
linkContext?: string;
}): Promise<void> {
const linkToken = params.linkToken?.trim();
if (!linkToken) return;
const allowedMobiles = await this.resolveAllowedMobiles(
linkToken,
params.linkContext,
);
if (allowedMobiles.length === 0) {
throwUserAuthError(UserAuthErrorCode.LINK_NOT_FOUND);
}
const normalizedMobile = normalizeIranMobile(params.mobile);
if (
!normalizedMobile ||
!allowedMobiles.some(
(mobile) => normalizeIranMobile(mobile) === normalizedMobile,
)
) {
throwUserAuthError(UserAuthErrorCode.LINK_MOBILE_MISMATCH);
}
}
async resolveAllowedMobiles(
linkToken: string,
linkContext?: string,
): Promise<string[]> {
if (!Types.ObjectId.isValid(linkToken)) return [];
const id = new Types.ObjectId(linkToken);
const context = this.normalizeContext(linkContext);
const allowedMobiles = new Set<string>();
const [legacyRequest, blameRequest, legacyClaim, claimCase] =
await Promise.all([
this.requestManagementModel.findById(id).lean().exec(),
this.blameRequestModel.findById(id).lean().exec(),
this.claimRequestManagementModel.findById(id).lean().exec(),
this.claimCaseModel.findById(id).lean().exec(),
]);
this.addLegacyRequestPhones(allowedMobiles, legacyRequest, context);
this.addBlameRequestPhones(allowedMobiles, blameRequest, context);
await this.addLegacyClaimOwnerPhone(allowedMobiles, legacyClaim);
await this.addClaimCaseOwnerPhone(allowedMobiles, claimCase);
return Array.from(allowedMobiles);
}
private addLegacyRequestPhones(
allowedMobiles: Set<string>,
legacyRequest: any,
context?: string,
) {
if (!legacyRequest) return;
const shouldAddFirst = !context || this.isFirstContext(context);
const shouldAddSecond = !context || this.isSecondContext(context);
if (shouldAddFirst) {
this.addPhone(
allowedMobiles,
legacyRequest.firstPartyDetails?.firstPartyPhoneNumber,
);
}
if (shouldAddSecond) {
this.addPhone(
allowedMobiles,
legacyRequest.secondPartyDetails?.secondPartyPhoneNumber,
);
}
for (const event of legacyRequest.history || []) {
const metadata = event?.metadata;
if (shouldAddSecond) this.addPhone(allowedMobiles, metadata?.secondPartyPhone);
for (const sent of metadata?.sentTo || []) {
if (!context || this.matchesRoleContext(context, sent?.role)) {
this.addPhone(allowedMobiles, sent?.phoneNumber);
}
}
}
}
private addBlameRequestPhones(
allowedMobiles: Set<string>,
blameRequest: any,
context?: string,
) {
if (!blameRequest) return;
for (const party of blameRequest.parties || []) {
if (context && !this.matchesRoleContext(context, party?.role)) continue;
this.addPhone(allowedMobiles, party?.person?.phoneNumber);
}
}
private async addLegacyClaimOwnerPhone(
allowedMobiles: Set<string>,
claimRequest: any,
) {
if (!claimRequest) return;
const ownerUserId = claimRequest.owner?.userId || claimRequest.userId;
if (!ownerUserId) return;
const ownerUserIdText = String(ownerUserId);
if (claimRequest.blameRequestId) {
const blameRequest = await this.blameRequestModel
.findById(claimRequest.blameRequestId)
.lean()
.exec();
const ownerParty = (blameRequest?.parties || []).find(
(party: any) =>
party?.person?.userId && String(party.person.userId) === ownerUserIdText,
);
this.addPhone(allowedMobiles, ownerParty?.person?.phoneNumber);
}
if (Types.ObjectId.isValid(ownerUserIdText)) {
const user = await this.userDbService.findOne({
_id: new Types.ObjectId(ownerUserIdText),
});
this.addPhone(allowedMobiles, user?.mobile);
this.addPhone(allowedMobiles, user?.username);
}
}
/** V2 `claimCases` — token in `/caseClaim?token=...` SMS links. */
private async addClaimCaseOwnerPhone(
allowedMobiles: Set<string>,
claimCase: any,
) {
if (!claimCase?.owner?.userId) return;
const ownerUserIdText = String(claimCase.owner.userId);
if (claimCase.blameRequestId) {
const blameRequest = await this.blameRequestModel
.findById(claimCase.blameRequestId)
.lean()
.exec();
const ownerParty = (blameRequest?.parties || []).find(
(party: any) =>
party?.person?.userId && String(party.person.userId) === ownerUserIdText,
);
this.addPhone(allowedMobiles, ownerParty?.person?.phoneNumber);
}
if (Types.ObjectId.isValid(ownerUserIdText)) {
const user = await this.userDbService.findOne({
_id: new Types.ObjectId(ownerUserIdText),
});
this.addPhone(allowedMobiles, user?.mobile);
this.addPhone(allowedMobiles, user?.username);
}
}
private addPhone(allowedMobiles: Set<string>, phone?: string) {
const normalized = normalizeIranMobile(phone);
if (normalized) allowedMobiles.add(normalized);
}
private normalizeContext(linkContext?: string): string | undefined {
const ctx = linkContext?.trim().toUpperCase();
if (!ctx) return undefined;
if (ctx === "USER" || ctx === "USER1") return "FIRST";
if (ctx === "USER2") return "SECOND";
if (ctx === "CASECLAIM" || ctx === "CLAIM") return undefined;
return ctx;
}
private matchesRoleContext(context: string, role?: string): boolean {
if (this.isFirstContext(context)) return role === PartyRole.FIRST;
if (this.isSecondContext(context)) return role === PartyRole.SECOND;
return true;
}
private isFirstContext(context: string): boolean {
return ["FIRST", "USER", "USER1", "FIRST_PARTY"].includes(context);
}
private isSecondContext(context: string): boolean {
return ["SECOND", "USER2", "SECOND_PARTY"].includes(context);
}
}

View File

@@ -1,21 +1,34 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
Logger,
NotAcceptableException,
NotFoundException,
} from "@nestjs/common";
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Types } from "mongoose";
import {
UserAuthErrorCode,
throwUserAuthError,
} from "src/auth/auth-services/user-auth-error";
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
import { LoginDtoRs } from "src/auth/dto/user/login.dto";
import {
buildUserLookupByPhone,
normalizeIranMobile,
} from "src/helpers/iran-mobile";
import {
computeOtpExpireMs,
FAKE_OTP_CODE,
isFakeOtpEnabled,
isOtpExpiryActive,
readOtpExpireMinutesFromEnv,
} from "src/helpers/user-otp-expiry";
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
import { SmsSendLogService } from "src/sms-orchestration/entities/db-service/sms-send-log.service";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
import { HashService } from "src/utils/hash/hash.service";
import { OtpService } from "src/utils/otp/otp.service";
import { SmsManagerService } from "src/utils/sms-manager/sms-manager.service";
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
export interface LinkBinding {
linkToken?: string;
linkContext?: string;
}
@Injectable()
export class UserAuthService {
private readonly logger = new Logger(UserAuthService.name);
@@ -24,18 +37,33 @@ export class UserAuthService {
private readonly jwtService: JwtService,
private readonly userDbService: UserDbService,
private readonly hashService: HashService,
private readonly otpCreator: OtpService,
private readonly smsManagerService: SmsManagerService,
private readonly otpCreator: OtpGeneratorService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly smsSendLogService: SmsSendLogService,
private readonly userLinkAccessService: UserLinkAccessService,
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userDbService.findOne({ username });
if (!user) throw new NotFoundException("user not found");
async validateUser(
username: string,
pass: string,
binding: LinkBinding = {},
): Promise<any> {
const canonicalMobile = normalizeIranMobile(username) ?? username.trim();
const now = new Date().getTime();
if (user.otp == null) throw new NotAcceptableException("please get otp");
if (user.otpExpire < now) {
throw new NotAcceptableException("expire otp");
await this.userLinkAccessService.assertMobileAllowed({
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const user = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
if (!user) throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
if (user.otp == null) throwUserAuthError(UserAuthErrorCode.OTP_REQUIRED);
if (!isOtpExpiryActive(user.otpExpire)) {
throwUserAuthError(UserAuthErrorCode.OTP_EXPIRED);
}
if (await this.hashService.compare(pass, user.otp)) {
return user;
@@ -44,39 +72,59 @@ export class UserAuthService {
}
async login(user: any) {
const userId = String(user._id ?? user.id ?? "");
const payload = {
username: user.username,
sub: user.id,
sub: userId,
role: "user",
};
const accToken = this.jwtService.sign(payload, {
secret: `${process.env.SECRET}`,
secret: `${process.env.JWT_SECRET}`, expiresIn: '1h'
});
await this.userDbService.findOneAndUpdate(
{ username: user.username },
{
tokens: { token: accToken },
otp: null,
otpExpire: 0,
},
);
return {
userId: user._id,
userId,
access_token: accToken,
};
}
async sendOtpRequest(mobile: string): Promise<LoginDtoRs> {
const userExist = await this.userDbService.findOne({
mobile,
async sendOtpRequest(
mobile: string,
binding: LinkBinding = {},
): Promise<LoginDtoRs> {
const canonicalMobile = normalizeIranMobile(mobile) ?? mobile.trim();
if (!canonicalMobile) {
throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
}
await this.userLinkAccessService.assertMobileAllowed({
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const otp = this.otpCreator.create();
const userExist = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
const otp = this.createOtpForRequest();
const hashOtp = await this.hashService.hash(otp);
const expireMinutes = readOtpExpireMinutesFromEnv();
const nowMs = Date.now();
const otpExpire = computeOtpExpireMs(expireMinutes, nowMs);
if (!userExist) {
await this.smsSender(otp, mobile);
/// create otp request
await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
const newUser = await this.userDbService.createUser({
mobile,
username: mobile,
mobile: canonicalMobile,
username: canonicalMobile,
otp: hashOtp,
tokens: {
token: "",
@@ -90,50 +138,61 @@ export class UserAuthService {
city: "",
address: "",
state: "",
otpExpire: new Date(
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
).getTime(),
otpExpire,
});
return new LoginDtoRs(newUser);
}
if (userExist) {
if (userExist.otpExpire > new Date(new Date().getTime()).getTime()) throw new BadRequestException("Wait for expiry time to finish");
await this.smsSender(otp, mobile);
const updateTokens = await this.userDbService.findOneAndUpdate(
{
username: userExist.username,
},
{
otp: hashOtp,
otpExpire: new Date(
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
).getTime(),
},
);
if (updateTokens) return new LoginDtoRs(userExist);
if (isOtpExpiryActive(userExist.otpExpire, nowMs)) {
// throwUserAuthError(UserAuthErrorCode.OTP_REQUEST_TOO_SOON);
return new LoginDtoRs(userExist, "OTP Still valid");
}
await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
await this.userDbService.findOneAndUpdate(
buildUserLookupByPhone(canonicalMobile),
{
otp: hashOtp,
otpExpire,
mobile: canonicalMobile,
username: userExist.username || canonicalMobile,
},
);
return new LoginDtoRs(userExist);
}
private createOtpForRequest(): string {
if (isFakeOtpEnabled()) {
this.logger.warn(
"FAKE_OTP=true — using fixed dev OTP; SMS provider is not called",
);
return FAKE_OTP_CODE;
}
return this.otpCreator.create();
}
private async smsSender(otp: string, mobile: string) {
return this.smsManagerService
.verifyLookUp({
token: otp,
template: process.env.AUTH_SMS_TEMPLATE,
if (isFakeOtpEnabled()) {
this.logger.log(
`FAKE_OTP=true — skipped SMS for phone=${mobile} (use OTP ${FAKE_OTP_CODE})`,
);
await this.smsSendLogService.recordSkippedOtp({
receptor: mobile,
})
.then((smsRes) => {
this.logger.log(
`${"phone : " + mobile + " " + ", status : " + smsRes["return"].status + ", otp : " + otp} `,
);
})
.catch((er) => {
this.logger.error(
`${"phone : " + mobile + " " + ", status : " + er["return"].status + ", otp : " + otp} `,
);
throw new HttpException(
" auth sms send failed",
HttpStatus.INTERNAL_SERVER_ERROR,
);
otp: FAKE_OTP_CODE,
});
return;
}
const ok = await this.smsOrchestrationService.sendAuthOtp(
mobile,
otp,
process.env.AUTH_SMS_TEMPLATE,
);
if (!ok) {
throw new HttpException("auth sms send failed", HttpStatus.BAD_GATEWAY);
}
this.logger.log(
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
);
}
}

View File

@@ -1,42 +1,82 @@
import { Module } from "@nestjs/common";
import { Global, Module } from "@nestjs/common";
import { JwtModule, JwtService } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { LocalStrategy } from "src/auth/stratregys/local.strategy";
import { LocalActorStrategy } from "src/auth/stratregys/local-actor.strategy";
import { MongooseModule } from "@nestjs/mongoose";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { LocalUserAuthGuard } from "src/auth/guards/user-local.guard";
import { ActorAuthController } from "src/auth/auth-controllers/actor/actor.auth.controller";
import { UserAuthController } from "src/auth/auth-controllers/user/user.auth.controller";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
import {
ClaimCase,
ClaimCaseSchema,
} from "src/claim-request-management/entites/schema/claim-cases.schema";
import {
ClaimRequestManagementModel,
ClaimRequestManagementSchema,
} from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { ClientModule } from "src/client/client.module";
import {
BlameRequest,
BlameRequestSchema,
} from "src/request-management/entities/schema/blame-cases.schema";
import {
RequestManagementModel,
RequestManagementSchema,
} from "src/request-management/entities/schema/request-management.schema";
import { UsersModule } from "src/users/users.module";
import { HashModule } from "src/utils/hash/hash.module";
// import { MailModule } from "src/utils/mail/mail.module";
import { OtpModule } from "src/utils/otp/otp.module";
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
import { CaptchaModule } from "src/captcha/captcha.module";
import { SuperAdminDbService } from "src/super-admin/entities/db-service/super-admin.db.service";
import {
SuperAdminModel,
SuperAdminSchema,
} from "src/super-admin/entities/schema/super-admin.schema";
/** Auth services and guards are app-wide (avoids importing AuthModule in every feature module). */
@Global()
@Module({
imports: [
// MailModule, // Mailer disabled – not used
UsersModule,
ClientModule,
HashModule,
OtpModule,
PassportModule,
SmsManagerModule,
CaptchaModule,
SmsOrchestrationModule,
MongooseModule.forFeature([
{ name: RequestManagementModel.name, schema: RequestManagementSchema },
{ name: BlameRequest.name, schema: BlameRequestSchema },
{
name: ClaimRequestManagementModel.name,
schema: ClaimRequestManagementSchema,
},
{ name: ClaimCase.name, schema: ClaimCaseSchema },
{ name: SuperAdminModel.name, schema: SuperAdminSchema },
]),
JwtModule.register({
signOptions: { expiresIn: "1h" },
signOptions: { expiresIn: "1h" }, // TODO: MAKE IT ENV
global: true,
secret: `${process.env.SECRET}`,
secret: `${process.env.JWT_SECRET}`,
}),
],
providers: [
UserAuthService,
UserLinkAccessService,
ActorAuthService,
LocalStrategy,
LocalActorStrategy,
JwtService,
LocalActorAuthGuard,
LocalUserAuthGuard,
SuperAdminDbService,
],
exports: [
UserAuthService,
ActorAuthService,
JwtService,
LocalActorAuthGuard,
LocalUserAuthGuard,
SuperAdminDbService,
],
exports: [LocalStrategy, UserAuthService, ActorAuthService, JwtService],
controllers: [UserAuthController, ActorAuthController],
})
export class AuthModule {}

View File

@@ -0,0 +1,18 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsMongoId, IsString, MinLength } from "class-validator";
export class CreateRegistrarDto {
@ApiProperty({ example: "registrar@sample.com" })
@IsEmail()
email: string;
@ApiProperty({ example: "securePassword123", minLength: 6 })
@IsString()
@MinLength(6)
password: string;
@ApiProperty({ example: "507f1f77bcf86cd799439011" })
@IsMongoId()
clientId: string;
}

View File

@@ -1,15 +1,55 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
import { RoleEnum } from "src/Types&Enums/role.enum";
export class LoginActorDto {
@ApiProperty({ example: RoleEnum, type: "array", description: "LOGIN_DTO" })
role: RoleEnum[];
@ApiProperty({})
username: string;
@ApiPropertyOptional({
description:
"Actor email or username. For field experts you may also send nationalCode instead.",
})
@IsOptional()
@IsString()
username?: string;
@ApiPropertyOptional({
description: "Alias for username when logging in with email.",
})
@IsOptional()
@IsString()
email?: string;
@ApiPropertyOptional({
example: "4311402422",
description:
"10-digit national ID. Alternative login identifier for actors (especially field experts without email).",
})
@IsOptional()
@IsString()
nationalCode?: string;
@ApiProperty({})
password: string;
@ApiProperty({
example: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
description: "Captcha id from GET /actor/captcha.",
})
@IsString()
@IsNotEmpty()
@MaxLength(64)
captchaId: string;
@ApiProperty({
example: "a7bx2",
description: "Characters shown in the captcha image.",
})
@IsString()
@IsNotEmpty()
@MaxLength(16)
captcha: string;
}
export class LoginActorDtoRs extends LoginActorDto {

View File

@@ -0,0 +1,28 @@
import { ApiProperty } from "@nestjs/swagger";
export class CaptchaResponseDto {
@ApiProperty({
description: "Captcha challenge id — send back with POST /actor/login.",
example: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
})
captchaId: string;
@ApiProperty({
description: "Sample text",
example: "sb20xe"
})
text: string
@ApiProperty({
description:
"SVG captcha as a data URI — use as `<img src={image} />` in the frontend.",
example: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iLi4u",
})
image: string;
@ApiProperty({
description: "Unix timestamp (ms) when this captcha expires.",
example: 1710000000000,
})
expiresAt: number;
}

View File

@@ -0,0 +1,15 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsIn, IsOptional } from "class-validator";
export class GetCaptchaImageQueryDto {
@ApiPropertyOptional({
enum: ["json", "raw"],
default: "json",
description:
"On GET /actor/captcha, use `raw` to return image/svg+xml (for browser preview). " +
"The JSON response includes `captchaId` either way.",
})
@IsOptional()
@IsIn(["json", "raw"])
format?: "json" | "raw";
}

View File

@@ -1,17 +1,43 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
import { UserModel } from "src/users/entities/schema/user.schema";
export class UserLoginDto {
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
description: "Mobile number (username for OTP login)",
})
@IsString()
@IsNotEmpty()
@MaxLength(20)
mobile: string;
@ApiPropertyOptional({
example: "65f0c7f0c3f8a2a7c8b3d001",
type: "string",
description: "Raw token from linked SMS URL (?token=...).",
})
@IsOptional()
@IsString()
@MaxLength(128)
linkToken?: string;
@ApiPropertyOptional({
example: "FIRST",
type: "string",
description: "Optional route/context hint for linked SMS login.",
})
@IsOptional()
@IsString()
@MaxLength(64)
linkContext?: string;
}
export class LoginDtoRs extends UserModel {
@ApiProperty({ type: "string" })
message: string;
@ApiProperty({
example: "09226187419",
type: "string",
@@ -37,8 +63,10 @@ export class LoginDtoRs extends UserModel {
username: string;
mobile: string;
nationalCode: string;
constructor(loginData) {
constructor(loginData, message: string = "") {
super();
this.mobile = loginData.mobile;
this.message = message;
}
}

View File

@@ -1,17 +1,44 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
export class UserVerifyOtp {
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
description: "Mobile number (same value sent to send-otp)",
})
@IsString()
@IsNotEmpty()
@MaxLength(20)
username: string;
@ApiProperty({
example: "258567",
type: "string",
description: "User login verify dto",
description: "OTP code from SMS",
})
@IsString()
@IsNotEmpty()
@MaxLength(16)
password: string;
@ApiPropertyOptional({
example: "65f0c7f0c3f8a2a7c8b3d001",
type: "string",
description: "Raw token from linked SMS URL (?token=...).",
})
@IsOptional()
@IsString()
@MaxLength(128)
linkToken?: string;
@ApiPropertyOptional({
example: "FIRST",
type: "string",
description: "Optional route/context hint for linked SMS login.",
})
@IsOptional()
@IsString()
@MaxLength(64)
linkContext?: string;
}

View File

@@ -1,41 +1,39 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
@Injectable()
export class LocalActorAuthGuard extends AuthGuard("actor") {
export class LocalActorAuthGuard implements CanActivate {
constructor(
private readonly actorAuthService: ActorAuthService,
private readonly jwtService: JwtService,
) {
super();
}
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
const path = request.url;
if (!token) {
if (path === "/actor/login") {
const loginData = await this.actorAuthService.loginActors(request.body);
if (this.isActorLoginRequest(request)) {
const loginData = await this.actorAuthService.loginFromCredentials(
request.body ?? {},
);
request.user = loginData;
request.identity = request;
request.identity = loginData;
return true;
} else {
throw new UnauthorizedException("Token not found");
}
throw new UnauthorizedException("Token not found");
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
secret: `${process.env.JWT_SECRET}`,
});
if (
@@ -44,6 +42,11 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
RoleEnum.DAMAGE_EXPERT,
RoleEnum.COMPANY,
RoleEnum.FIELD_EXPERT,
RoleEnum.REGISTRAR,
RoleEnum.FILE_MAKER,
RoleEnum.FILE_REVIEWER,
RoleEnum.SUPER_ADMIN,
RoleEnum.CALL_CENTER,
].includes(payload.role)
) {
throw new UnauthorizedException("User role is not authorized");
@@ -58,9 +61,21 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
//@ts-ignore
const [type, token] = request.headers.authorization?.split(" ") ?? [];
private isActorLoginRequest(request: {
url?: string;
path?: string;
route?: { path?: string };
}): boolean {
const path = (request.route?.path ?? request.url ?? request.path ?? "")
.split("?")[0]
.replace(/\/+$/, "");
return path === "/actor/login" || path.endsWith("/actor/login");
}
private extractTokenFromHeader(request: {
headers?: { authorization?: string };
}): string | undefined {
const [type, token] = request.headers?.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -31,7 +31,7 @@ export class ClaimAccessGuard implements CanActivate {
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
secret: `${process.env.JWT_SECRET}`,
});
// Allow users to pass through (they will be checked by service methods)
@@ -81,7 +81,10 @@ export class ClaimAccessGuard implements CanActivate {
throw new UnauthorizedException("Invalid role");
} catch (error) {
if (error instanceof ForbiddenException || error instanceof UnauthorizedException) {
if (
error instanceof ForbiddenException ||
error instanceof UnauthorizedException
) {
throw error;
}
throw new UnauthorizedException();
@@ -124,4 +127,3 @@ export class ClaimAccessGuard implements CanActivate {
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -8,31 +8,44 @@ import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
import { RoleEnum } from "src/Types&Enums/role.enum";
const GLOBAL_GUARD_ROLES = new Set<string>([
RoleEnum.USER,
RoleEnum.FIELD_EXPERT,
RoleEnum.REGISTRAR,
RoleEnum.FILE_MAKER,
RoleEnum.FILE_REVIEWER
]);
@Injectable()
export class GlobalGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
throw new UnauthorizedException("Missing Bearer token");
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
secret: `${process.env.JWT_SECRET}`,
});
if (payload.role !== RoleEnum.USER) {
console.log(
"🚀 ~ GlobalGuard ~ canActivate ~ request.user.role:",
request.user.role,
if (!payload?.role || !GLOBAL_GUARD_ROLES.has(String(payload.role))) {
throw new UnauthorizedException(
`Role "${payload?.role ?? "unknown"}" is not allowed on user-panel APIs. Use /user/login for USER, or /actor/login for experts.`,
);
throw new UnauthorizedException();
}
request.user = payload;
request.identity = request.user;
} catch {
throw new UnauthorizedException();
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException("Invalid or expired token");
}
return true;
}

View File

@@ -5,16 +5,19 @@ import { Reflector } from "@nestjs/core";
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// get the roles required
const roles = this.reflector.getAllAndOverride<string[]>("role", [
context.getHandler(),
context.getClass(),
]);
if (!roles) {
if (!roles?.length) {
return false;
}
const request = context.switchToHttp().getRequest();
const userRoles = request.user?.role?.split(",");
const role = request.user?.role;
if (!role) {
return false;
}
const userRoles = String(role).split(",");
return this.validateRoles(roles, userRoles);
}

View File

@@ -0,0 +1,41 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
/**
* Verifies Bearer JWT for platform settings routes. Does not restrict by role;
* pair with {@link RolesGuard} on handlers.
*/
@Injectable()
export class SettingsJwtGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException("Token not found");
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.JWT_SECRET}`,
});
(request as any).user = payload;
(request as any).identity = payload;
return true;
} catch {
throw new UnauthorizedException("Invalid token");
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -1,27 +1,30 @@
import {
CanActivate,
ExecutionContext,
Injectable,
NotAcceptableException,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import {
UserAuthErrorCode,
throwUserAuthError,
} from "src/auth/auth-services/user-auth-error";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
@Injectable()
export class LocalUserAuthGuard extends AuthGuard("local") {
constructor(private readonly userAuthService: UserAuthService) {
super();
}
export class LocalUserAuthGuard implements CanActivate {
constructor(private readonly userAuthService: UserAuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { username, password } = request.body;
let isValidUser = await this.userAuthService.validateUser(
const { username, password, linkToken, linkContext } = request.body ?? {};
const isValidUser = await this.userAuthService.validateUser(
username,
password,
{ linkToken, linkContext },
);
if (!isValidUser) {
throw new NotAcceptableException("otp is wrong");
throwUserAuthError(UserAuthErrorCode.OTP_INVALID);
}
request["user"] = isValidUser;
request.user = isValidUser;
return true;
}
}

View File

@@ -1,22 +0,0 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
@Injectable()
export class LocalActorStrategy extends PassportStrategy(Strategy, "actor") {
constructor(private readonly actorAuthService: ActorAuthService) {
super();
}
// async validate(username, password): Promise<any> {
// const user = await this.actorAuthService.validateActor(
// username,
// password,
// );
// if (!user) {
// throw new UnauthorizedException("user not found");
// }
// return user;
// }
}

View File

@@ -1,19 +0,0 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userAuthService: UserAuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.userAuthService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException("user not found please register");
}
return user;
}
}

View File

@@ -0,0 +1,112 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { randomUUID } from "node:crypto";
import {
CaptchaAuthErrorCode,
throwCaptchaAuthError,
} from "src/auth/auth-services/captcha-auth.error";
import { CaptchaResponseDto } from "src/auth/dto/captcha-response.dto";
import { CaptchaService } from "src/captcha/captcha.service";
import { CaptchaChallengeDbService } from "src/captcha/entities/db-service/captcha-challenge.db.service";
import { HashService } from "src/utils/hash/hash.service";
@Injectable()
export class CaptchaChallengeService {
private readonly isDev: boolean;
private readonly captchaEnabled: boolean;
constructor(
private readonly captchaService: CaptchaService,
private readonly hashService: HashService,
private readonly captchaChallengeDbService: CaptchaChallengeDbService,
private readonly configService: ConfigService,
) {
this.isDev = this.configService.get<string>("NODE_ENV") === "development";
this.captchaEnabled = this.configService.get<string>("CAPTCHA_ENABLED") !== "false";
}
async issue(): Promise<CaptchaResponseDto> {
const generated = this.captchaService.generate();
const captchaId = randomUUID();
// Always hash and persist the answer — the env check was the root bug
const answerHash = await this.hashService.hash(
this.captchaService.normalizeAnswer(generated.text),
);
// expireAt is the MongoDB TTL sentinel. The TTL reaper fires every ~60 s, so
// setting it equal to expiresAt means Mongo can delete the document up to 60 s
// BEFORE the application-level expiry check runs — causing the intermittent
// "captchaId not found" error under load. Adding a 120 s grace buffer ensures
// the document is always present when verify() runs its own expiresAt check.
await this.captchaChallengeDbService.create({
captchaId,
answerHash,
image: generated.image,
expiresAt: generated.expiresAt,
expireAt: new Date(generated.expiresAt + 120_000),
usedAt: null,
});
return {
captchaId,
image: generated.image,
expiresAt: generated.expiresAt,
...(this.isDev && { text: generated.text }),
};
}
async getImageById(captchaId: string): Promise<string> {
const challenge =
await this.captchaChallengeDbService.findByCaptchaId(captchaId);
if (!challenge) {
throw new NotFoundException("Captcha not found");
}
return this.decodeImage(challenge.image);
}
async verify(
captchaId: string | undefined,
answer: string | undefined,
): Promise<void> {
// Skip captcha verification if disabled via environment variable
if (!this.captchaEnabled) {
return;
}
if (!captchaId?.trim()) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
}
if (!answer?.trim()) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
}
const challenge = await this.captchaChallengeDbService.findByCaptchaId(
captchaId.trim(),
);
if (!challenge) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND);
}
if (challenge.usedAt) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_INVALID);
}
if (challenge.expiresAt < Date.now()) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_EXPIRED);
}
const ok = await this.hashService.compare(
this.captchaService.normalizeAnswer(answer),
challenge.answerHash,
);
if (!ok) {
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_INVALID);
}
await this.captchaChallengeDbService.markUsed(challenge.captchaId);
}
private decodeImage(imageDataUri: string): string {
const base64 = imageDataUri.replace(/^data:image\/svg\+xml;base64,/, "");
return Buffer.from(base64, "base64").toString("utf8");
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { HashModule } from "src/utils/hash/hash.module";
import { CaptchaChallengeService } from "./captcha-challenge.service";
import { CaptchaService } from "./captcha.service";
import { CaptchaChallengeDbService } from "./entities/db-service/captcha-challenge.db.service";
import {
CaptchaChallenge,
CaptchaChallengeSchema,
} from "./entities/schema/captcha-challenge.schema";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [
ConfigModule,
HashModule,
MongooseModule.forFeature([
{ name: CaptchaChallenge.name, schema: CaptchaChallengeSchema },
]),
],
providers: [
CaptchaService,
CaptchaChallengeDbService,
CaptchaChallengeService,
],
exports: [CaptchaChallengeService],
})
export class CaptchaModule {}

View File

@@ -0,0 +1,43 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import * as svgCaptcha from "svg-captcha";
export interface GeneratedCaptcha {
text: string;
image: string;
expiresAt: number;
}
@Injectable()
export class CaptchaService {
generate(): GeneratedCaptcha {
const captcha = svgCaptcha.create({
size: 5,
ignoreChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
noise: 1,
color: false,
background: "#f8fafc",
width: 160,
height: 56,
fontSize: 52,
});
return {
text: captcha.text,
image: `data:image/svg+xml;base64,${Buffer.from(captcha.data, "utf8").toString("base64")}`,
expiresAt: this.buildExpireAt(),
};
}
normalizeAnswer(input: string): string {
return input.trim().toLowerCase();
}
buildExpireAt(): number {
const raw = Number(
process.env.EXP_CAPTCHA_TIME ?? process.env.EXP_OTP_TIME ?? "2",
);
const minutes = Number.isFinite(raw) && raw > 0 ? raw : 2;
return Date.now() + minutes * 60 * 1000;
}
}

View File

@@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import {
CaptchaChallenge,
CaptchaChallengeDocument,
} from "../schema/captcha-challenge.schema";
@Injectable()
export class CaptchaChallengeDbService {
constructor(
@InjectModel(CaptchaChallenge.name)
private readonly captchaChallengeModel: Model<CaptchaChallengeDocument>,
) {}
create(data: CaptchaChallenge): Promise<CaptchaChallengeDocument> {
return this.captchaChallengeModel.create(data);
}
findByCaptchaId(
captchaId: string,
): Promise<CaptchaChallengeDocument | null> {
return this.captchaChallengeModel.findOne({ captchaId });
}
markUsed(captchaId: string): Promise<CaptchaChallengeDocument | null> {
return this.captchaChallengeModel.findOneAndUpdate(
{ captchaId, usedAt: null },
{ $set: { usedAt: new Date() } },
{ new: true },
);
}
}

View File

@@ -0,0 +1,32 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
@Schema({
collection: "captcha-challenges",
timestamps: true,
versionKey: false,
})
export class CaptchaChallenge {
@Prop({ required: true, unique: true, index: true })
captchaId: string;
@Prop({ required: true })
answerHash: string;
@Prop({ required: true })
image: string;
@Prop({ required: true, index: true })
expiresAt: number;
/** Mongo TTL — document removed shortly after this time. */
@Prop({ required: true, expires: 0 })
expireAt: Date;
@Prop({ default: null })
usedAt?: Date | null;
}
export type CaptchaChallengeDocument = HydratedDocument<CaptchaChallenge>;
export const CaptchaChallengeSchema =
SchemaFactory.createForClass(CaptchaChallenge);

View File

@@ -0,0 +1,233 @@
import { buildInsurerFileReport } from "./case-expert-report.builder";
import { PR } from "./persian-report-labels";
import { toJalaliDateAndTime } from "../helpers/date-jalali";
describe("buildInsurerFileReport", () => {
const getFieldValue = (
report: ReturnType<typeof buildInsurerFileReport>,
sectionTitle: string,
fieldLabel: string,
) => {
const section = report.sections.find((item) => item.title === sectionTitle);
expect(section).toBeDefined();
const field = section?.fields.find((item) => item.label === fieldLabel);
expect(field).toBeDefined();
return field?.value;
};
it("separates insurance blocks and exposes Fanavaran/timeline/evaluation data", () => {
const fileCreatedAt = new Date("2026-08-10T09:53:23.588Z");
const evaluationSubmittedAt = new Date("2026-08-10T12:30:45.000Z");
const [fileDate, fileTime] = toJalaliDateAndTime(fileCreatedAt);
const [evaluationDate, evaluationTime] =
toJalaliDateAndTime(evaluationSubmittedAt);
const report = buildInsurerFileReport({
overview: {
publicId: "A00010",
requestNo: "REQ-10",
createdAt: fileCreatedAt,
},
blame: {
type: "THIRD_PARTY",
blameStatus: "AGREED",
createdAt: fileCreatedAt,
parties: [
{
role: "FIRST",
person: {
userId: "guilty-user-id",
fullName: "مقصر نمونه",
phoneNumber: "09120000000",
nationalCodeOfInsurer: "0987654321",
insurerBirthday: 13650115,
clientId: "client-guilty",
},
statement: {
admitsGuilt: true,
acceptsExpertOpinion: true,
description: "توضیحات مقصر",
},
vehicle: {
carName: "206",
carModel: "1401",
plate: {
leftDigits: 98,
centerAlphabet: "ج",
centerDigits: 765,
ir: 22,
},
},
insurance: {
policyNumber: "TP-GUILTY-001",
company: "بیمه ثالث مقصر",
startDate: "1405/01/01",
endDate: "1406/01/01",
financialCeiling: "900000000",
carBodyInsurance: {
policyNumber: "CB-GUILTY-001",
insurerCompany: "بیمه بدنه مقصر",
startDate: "1405/02/01",
endDate: "1406/02/01",
},
},
},
{
role: "SECOND",
person: {
userId: "damaged-user-id",
fullName: "زیان دیده نمونه",
phoneNumber: "09121111111",
nationalCodeOfInsurer: "1234567890",
clientId: "client-damaged",
insurerBirthday: 13700101,
},
statement: {
claimsDamage: true,
acceptsExpertOpinion: false,
description: "توضیحات زیان‌دیده",
accidentDate: "2026-08-10",
accidentTime: "13:23",
},
insurance: {
policyNumber: "TP-DAMAGED-001",
company: "بیمه ثالث زیان‌دیده",
startDate: "1405/03/01",
endDate: "1406/03/01",
financialCeiling: "700000000",
carBodyInsurance: {
policyNumber: "CB-DAMAGED-001",
insurerCompany: "بیمه بدنه زیان‌دیده",
startDate: "1405/04/01",
endDate: "1406/04/01",
coverages: ["سرقت", "آتش‌سوزی"],
},
},
vehicle: {
carName: "207",
carModel: "1402",
plate: {
leftDigits: 12,
centerAlphabet: "ب",
centerDigits: 345,
ir: 11,
},
},
},
],
expert: {
decision: {
guiltyPartyId: "guilty-user-id",
description: "مقصر شناخته شد",
fields: {
accidentWay: { label: "از جلو" },
accidentReason: { label: "عدم رعایت فاصله" },
accidentType: { label: "برخورد" },
},
},
},
},
claim: {
claimStatus: "APPROVED",
claimNo: 111,
claimId: 222,
dmgCaseId: 333,
expertiseId: 444,
owner: { fullName: "زیان دیده نمونه" },
vehicle: {
carName: "207",
carModel: "1402",
carType: "sedan",
},
fanavaranSync: {
baseClaim: {
policyId: 555,
driverId: 666,
vehicleKindId: 777,
insuranceCorpId: 888,
},
},
evaluation: {
damageExpertReplyFinal: {
submittedAt: evaluationSubmittedAt,
description: "نیاز به تعویض سپر جلو",
actorDetail: { actorName: "کارشناس خسارت نمونه" },
},
},
},
});
expect(getFieldValue(report, PR.ownerSection, PR.name)).toBe("زیان دیده نمونه");
expect(getFieldValue(report, PR.guiltyOwnerSection, PR.name)).toBe("مقصر نمونه");
expect(getFieldValue(report, PR.guiltyOwnerSection, PR.phone)).toBe("09120000000");
expect(getFieldValue(report, PR.guiltyOwnerSection, PR.nationalCode)).toBe(
"0987654321",
);
expect(getFieldValue(report, PR.damagedThirdPartyInsuranceSection, PR.policyNumber)).toBe(
"TP-DAMAGED-001",
);
expect(getFieldValue(report, PR.guiltyThirdPartyInsuranceSection, PR.policyNumber)).toBe(
"TP-GUILTY-001",
);
expect(getFieldValue(report, PR.damagedCarBodyInsuranceSection, PR.policyNumber)).toBe(
"CB-DAMAGED-001",
);
expect(getFieldValue(report, PR.guiltyCarBodyInsuranceSection, PR.policyNumber)).toBe(
"CB-GUILTY-001",
);
expect(getFieldValue(report, PR.damagedVehicleSection, "خودرو / نام خودرو")).toBe(
"207",
);
expect(getFieldValue(report, PR.guiltyVehicleSection, "خودرو / نام خودرو")).toBe(
"206",
);
expect(getFieldValue(report, PR.damagedStatementSection, PR.partyDescription)).toBe(
"توضیحات زیان‌دیده",
);
expect(getFieldValue(report, PR.damagedStatementSection, PR.claimsDamage)).toBe(
"بله",
);
expect(
getFieldValue(report, PR.damagedStatementSection, PR.acceptsExpertOpinion),
).toBe("خیر");
expect(getFieldValue(report, PR.guiltyStatementSection, PR.partyDescription)).toBe(
"توضیحات مقصر",
);
expect(getFieldValue(report, PR.guiltyStatementSection, PR.admitsGuilt)).toBe(
"بله",
);
expect(
getFieldValue(report, PR.guiltyStatementSection, PR.acceptsExpertOpinion),
).toBe("بله");
expect(getFieldValue(report, PR.fanavaranSection, PR.fanavaranClaimNo)).toBe(
"111",
);
expect(
getFieldValue(report, PR.fanavaranSection, PR.fanavaranExpertiseId),
).toBe("444");
expect(getFieldValue(report, PR.fanavaranSection, PR.fanavaranPolicyId)).toBe(
"555",
);
expect(getFieldValue(report, PR.timelineSection, PR.fileRegisteredAt)).toBe(
`${fileDate} ${fileTime}`,
);
expect(
getFieldValue(report, PR.timelineSection, PR.evaluationRegisteredAt),
).toBe(`${evaluationDate} ${evaluationTime}`);
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationResult)).toBe(
"تأیید شده",
);
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationExpert)).toBe(
"کارشناس خسارت نمونه",
);
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationResponse)).toBe(
"نیاز به تعویض سپر جلو",
);
});
});

View File

@@ -0,0 +1,919 @@
import {
resolveClaimOwnerParty,
resolveDamagedPartyRow,
} from "src/helpers/blame-damaged-party";
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import {
InsurerFileReportField,
InsurerFileReportSection,
InsurerFileReportViewModel,
} from "./case-expert-report.types";
import { PR, persianFieldPath, persianStatus } from "./persian-report-labels";
const SKIP_FLATTEN_KEYS = new Set([
"_id",
"__v",
"history",
"workflow",
"evidence",
"confirmation",
"inquiries",
"raw",
]);
type ReportRecord = Record<string, unknown>;
type ReportParty = ReportRecord & {
role?: string;
person?: ReportRecord;
insurance?: ReportRecord & { carBodyInsurance?: ReportRecord };
vehicle?: ReportRecord;
statement?: ReportRecord;
location?: { lat?: number; lon?: number };
};
function asString(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined;
if (value instanceof Date) {
const [d, t] = toJalaliDateAndTime(value);
return `${d} ${t}`;
}
if (typeof value === "object") {
if (typeof (value as { toString?: () => string }).toString === "function") {
const s = String(value);
if (s !== "[object Object]") return s;
}
return undefined;
}
return String(value);
}
function formatBirthDate(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined;
const raw = String(value);
if (/^\d{8}$/.test(raw)) {
return `${raw.slice(0, 4)}/${raw.slice(4, 6)}/${raw.slice(6, 8)}`;
}
return raw;
}
function formatDateTime(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined;
const date = value instanceof Date ? value : new Date(value as string | number);
if (Number.isNaN(date.getTime())) return asString(value);
const [d, t] = toJalaliDateAndTime(date);
return `${d} ${t}`;
}
function firstDefined(...values: unknown[]): string | undefined {
for (const value of values) {
const str = asString(value);
if (str) return str;
}
return undefined;
}
function normalizeListValue(value: unknown): string | undefined {
if (!Array.isArray(value) || !value.length) return undefined;
const items = value
.map((item) => asString(item) ?? JSON.stringify(item))
.filter(Boolean);
return items.length ? items.join("، ") : undefined;
}
function flattenObject(
obj: unknown,
prefix = "",
depth = 0,
): InsurerFileReportField[] {
if (obj == null) return [];
if (depth > 4) {
return [{ label: persianFieldPath(prefix), value: asString(obj) }];
}
if (Array.isArray(obj)) {
const value = normalizeListValue(obj);
return value
? [{ label: persianFieldPath(prefix || "items"), value }]
: [];
}
if (typeof obj !== "object") {
return [{ label: persianFieldPath(prefix || "value"), value: asString(obj) }];
}
const rows: InsurerFileReportField[] = [];
const objRecord = obj as Record<string, unknown>;
const hasMapped =
objRecord.mapped != null && typeof objRecord.mapped === "object";
for (const [key, value] of Object.entries(objRecord)) {
if (SKIP_FLATTEN_KEYS.has(key)) continue;
if (key === "raw" && hasMapped) continue;
if (value === undefined || value === null || value === "") continue;
const path = prefix ? `${prefix}.${key}` : key;
if (
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Date)
) {
rows.push(...flattenObject(value, path, depth + 1));
} else {
rows.push({ label: persianFieldPath(path), value: asString(value) });
}
}
return rows;
}
function dedupeFields(fields: InsurerFileReportField[]): InsurerFileReportField[] {
const seen = new Set<string>();
const out: InsurerFileReportField[] = [];
for (const field of fields) {
const value = asString(field.value);
if (!value) continue;
const key = `${field.label}::${value}`;
if (seen.has(key)) continue;
seen.add(key);
out.push({ label: field.label, value });
}
return out;
}
function filterEmptySections(
sections: Array<InsurerFileReportSection | undefined>,
): InsurerFileReportSection[] {
return sections.filter(
(section): section is InsurerFileReportSection => !!section && section.fields.length > 0,
);
}
function buildSection(
title: string,
fields: InsurerFileReportField[],
withPlaceholder = true,
): InsurerFileReportSection {
const deduped = dedupeFields(fields);
return {
title,
fields:
deduped.length || !withPlaceholder
? deduped
: [{ label: PR.data, value: PR.empty }],
};
}
function expertNameFromSnapshot(snapshot?: {
firstName?: string;
lastName?: string;
}): string | undefined {
if (!snapshot) return undefined;
const name = [snapshot.firstName, snapshot.lastName].filter(Boolean).join(" ");
return name || undefined;
}
function collectExpertNames(
blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
): string | undefined {
const names = new Set<string>();
const blameDecision = (
blame?.expert as Record<string, unknown> | undefined
)?.decision as Record<string, unknown> | undefined;
const blameExpert = expertNameFromSnapshot(
blameDecision?.expertProfileSnapshot as
| { firstName?: string; lastName?: string }
| undefined,
);
if (blameExpert) names.add(blameExpert);
const evaluation = claim?.evaluation as Record<string, unknown> | undefined;
for (const key of ["damageExpertReplyFinal", "damageExpertReply"] as const) {
const reply = evaluation?.[key] as Record<string, unknown> | undefined;
if (!reply) continue;
const actor = (reply.actorDetail as { actorName?: string } | undefined)
?.actorName;
if (actor) names.add(actor);
const snap = expertNameFromSnapshot(
reply.expertProfileSnapshot as
| { firstName?: string; lastName?: string }
| undefined,
);
if (snap) names.add(snap);
}
return names.size ? [...names].join(", ") : undefined;
}
function inquiryRoleData(
inquiries: Record<string, unknown> | undefined,
key: string,
role?: string,
): Record<string, unknown> | undefined {
const block = inquiries?.[key] as Record<string, unknown> | undefined;
const data = block?.data as Record<string, unknown> | undefined;
if (!data) return undefined;
if (role && data[role] && typeof data[role] === "object") {
return data[role] as Record<string, unknown>;
}
return data;
}
function pickInquiryReportPayload(
inquiry?: Record<string, unknown>,
): Record<string, unknown> | undefined {
if (!inquiry) return undefined;
const mapped = inquiry.mapped;
if (mapped && typeof mapped === "object" && !Array.isArray(mapped)) {
return mapped as Record<string, unknown>;
}
const { raw: _raw, ...rest } = inquiry;
return Object.keys(rest).length ? rest : inquiry;
}
function resolveReportBlameContext(
blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
): Record<string, unknown> | null {
if (blame) return blame;
const snapshot = claim?.snapshot as Record<string, unknown> | undefined;
if (!snapshot) return null;
return {
type:
(claim?.blameFileContext as Record<string, unknown> | undefined)
?.blameRequestType ??
(snapshot.accident as Record<string, unknown> | undefined)?.type,
blameStatus: (claim?.blameFileContext as Record<string, unknown> | undefined)
?.blameStatus,
parties: snapshot.parties,
};
}
function getPartyRole(party: ReportParty | null | undefined): string | undefined {
const role = party?.role;
return typeof role === "string" ? role : undefined;
}
function partyKindLabel(
party: ReportParty | null | undefined,
damagedParty: ReportParty | null,
guiltyParty: ReportParty | null,
): string | undefined {
if (sameParty(party, damagedParty)) return "damaged";
if (sameParty(party, guiltyParty)) return "guilty";
return undefined;
}
function partyRoleLabel(role: string | undefined): string | undefined {
if (!role) return undefined;
if (role === PartyRole.FIRST) return "طرف اول";
if (role === PartyRole.SECOND) return "طرف دوم";
return role;
}
function statementBoolean(value: unknown): string | undefined {
if (typeof value !== "boolean") return undefined;
return persianStatus(value);
}
function sameParty(
first: ReportParty | null | undefined,
second: ReportParty | null | undefined,
): boolean {
if (!first || !second) return false;
const firstUserId = first.person?.userId != null ? String(first.person.userId) : "";
const secondUserId =
second.person?.userId != null ? String(second.person.userId) : "";
if (firstUserId && secondUserId) return firstUserId === secondUserId;
return getPartyRole(first) === getPartyRole(second);
}
function resolveEvaluationReply(
claim?: Record<string, unknown> | null,
): Record<string, unknown> | undefined {
const evaluation = claim?.evaluation as Record<string, unknown> | undefined;
return (
(evaluation?.damageExpertReplyFinal as Record<string, unknown> | undefined) ??
(evaluation?.damageExpertReply as Record<string, unknown> | undefined)
);
}
function insuranceValueFromCandidates(
...values: unknown[]
): string | undefined {
for (const value of values) {
if (Array.isArray(value)) {
const listValue = normalizeListValue(value);
if (listValue) return listValue;
continue;
}
const str = asString(value);
if (str) return str;
}
return undefined;
}
function buildThirdPartyInsuranceFields(
party: ReportParty | null | undefined,
blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
): InsurerFileReportField[] {
const role = getPartyRole(party);
const direct = (party?.insurance ?? {}) as Record<string, unknown>;
const blameInquiry = pickInquiryReportPayload(
inquiryRoleData(blame?.inquiries as Record<string, unknown> | undefined, "thirdParty", role),
);
const claimInquiry = pickInquiryReportPayload(
inquiryRoleData(claim?.inquiries as Record<string, unknown> | undefined, "thirdParty", role),
);
return [
{
label: PR.policyNumber,
value: insuranceValueFromCandidates(
direct.policyNumber,
blameInquiry?.policyNumber,
blameInquiry?.PolicyNumber,
claimInquiry?.policyNumber,
claimInquiry?.PolicyNumber,
blameInquiry?.ThirdPolicyCode,
claimInquiry?.ThirdPolicyCode,
),
},
{
label: PR.insuranceCompany,
value: insuranceValueFromCandidates(
direct.company,
direct.insurerCompany,
blameInquiry?.company,
blameInquiry?.CompanyName,
claimInquiry?.company,
claimInquiry?.CompanyName,
),
},
{
label: PR.policyStartDate,
value: insuranceValueFromCandidates(
direct.startDate,
blameInquiry?.startDate,
blameInquiry?.PolicyStartDate,
blameInquiry?.SatrtDate,
claimInquiry?.startDate,
claimInquiry?.PolicyStartDate,
claimInquiry?.SatrtDate,
),
},
{
label: PR.policyEndDate,
value: insuranceValueFromCandidates(
direct.endDate,
blameInquiry?.endDate,
blameInquiry?.PolicyEndDate,
blameInquiry?.EndDate,
claimInquiry?.endDate,
claimInquiry?.PolicyEndDate,
claimInquiry?.EndDate,
),
},
{
label: PR.financialCeiling,
value: insuranceValueFromCandidates(
direct.financialCeiling,
blameInquiry?.financialCeiling,
blameInquiry?.FinancialCvrCptl,
blameInquiry?.FnCvrCptl,
claimInquiry?.financialCeiling,
claimInquiry?.FinancialCvrCptl,
claimInquiry?.FnCvrCptl,
),
},
{
label: PR.coverages,
value: insuranceValueFromCandidates(
direct.coverages,
blameInquiry?.coverages,
claimInquiry?.coverages,
),
},
];
}
function buildCarBodyInsuranceFields(
party: ReportParty | null | undefined,
blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
): InsurerFileReportField[] {
const role = getPartyRole(party);
const direct = (party?.insurance?.carBodyInsurance ??
party?.insurance?.carBody ??
{}) as Record<string, unknown>;
const blameInquiry = pickInquiryReportPayload(
inquiryRoleData(blame?.inquiries as Record<string, unknown> | undefined, "carBody", role),
);
const claimInquiry = pickInquiryReportPayload(
inquiryRoleData(claim?.inquiries as Record<string, unknown> | undefined, "carBody", role),
);
const legacy = (blame?.carBodyInsuranceDetail ?? {}) as Record<string, unknown>;
return [
{
label: PR.policyNumber,
value: insuranceValueFromCandidates(
direct.policyNumber,
legacy.policyNumber,
blameInquiry?.policyNumber,
blameInquiry?.PolicyNumber,
claimInquiry?.policyNumber,
claimInquiry?.PolicyNumber,
),
},
{
label: PR.insuranceCompany,
value: insuranceValueFromCandidates(
direct.insurerCompany,
direct.company,
legacy.insurerCompany,
blameInquiry?.company,
blameInquiry?.CompanyName,
claimInquiry?.company,
claimInquiry?.CompanyName,
),
},
{
label: PR.policyStartDate,
value: insuranceValueFromCandidates(
direct.startDate,
legacy.startDate,
blameInquiry?.startDate,
blameInquiry?.PolicyStartDate,
claimInquiry?.startDate,
claimInquiry?.PolicyStartDate,
),
},
{
label: PR.policyEndDate,
value: insuranceValueFromCandidates(
direct.endDate,
legacy.endDate,
blameInquiry?.endDate,
blameInquiry?.PolicyEndDate,
claimInquiry?.endDate,
claimInquiry?.PolicyEndDate,
),
},
{
label: PR.coverages,
value: insuranceValueFromCandidates(
direct.coverages,
legacy.coverages,
blameInquiry?.coverages,
claimInquiry?.coverages,
),
},
];
}
function buildPartyOwnerSection(
title: string,
party: ReportParty | null | undefined,
options?: {
claim?: Record<string, unknown> | null;
useClaimOwnerFallback?: boolean;
includeSheba?: boolean;
},
): InsurerFileReportSection | undefined {
const person = party?.person as ReportRecord | undefined;
const claim = options?.claim;
const money = claim?.money as
| { sheba?: string; nationalCodeOfInsurer?: string }
| undefined;
const claimOwner = claim?.owner as { fullName?: string } | undefined;
if (!person && !options?.useClaimOwnerFallback) return undefined;
return buildSection(title, [
{
label: PR.name,
value: options?.useClaimOwnerFallback
? firstDefined(person?.fullName, claimOwner?.fullName)
: firstDefined(person?.fullName),
},
{ label: PR.phone, value: asString(person?.phoneNumber) },
{
label: PR.nationalCode,
value: options?.useClaimOwnerFallback
? firstDefined(person?.nationalCodeOfInsurer, money?.nationalCodeOfInsurer)
: firstDefined(person?.nationalCodeOfInsurer, person?.nationalCode),
},
{
label: PR.birthDate,
value: formatBirthDate(
person?.insurerBirthday ?? person?.birthday ?? person?.driverBirthday,
),
},
{
label: PR.sheba,
value: options?.includeSheba ? money?.sheba : undefined,
},
]);
}
function licenseFieldsFromInquiry(
inquiry?: Record<string, unknown>,
): { licenseType?: string; licenseDate?: string } {
if (!inquiry) return {};
return {
licenseType: firstDefined(
inquiry.LicenseType,
inquiry.licenseType,
inquiry.Type,
inquiry.type,
inquiry.LicenseCategory,
inquiry.licenseCategory,
),
licenseDate: firstDefined(
inquiry.IssueDate,
inquiry.issueDate,
inquiry.LicenseIssueDate,
inquiry.licenseIssueDate,
inquiry.ExpireDate,
inquiry.expireDate,
),
};
}
function buildDriverSection(
damagedParty: ReportParty | null,
blame?: Record<string, unknown> | null,
): InsurerFileReportSection | undefined {
const person = damagedParty?.person as ReportRecord | undefined;
if (!person || person.driverIsInsurer !== false) return undefined;
const role = damagedParty?.role ?? PartyRole.FIRST;
const licenseInquiry = inquiryRoleData(
blame?.inquiries as Record<string, unknown> | undefined,
"drivingLicence",
String(role),
);
const { licenseType, licenseDate } = licenseFieldsFromInquiry(licenseInquiry);
return buildSection(PR.driverSection, [
{ label: PR.name, value: asString(person.fullName) },
{
label: PR.licenseType,
value: licenseType ?? (person.driverLicense ? PR.driverLicense : undefined),
},
{
label: PR.licenseDate,
value: licenseDate ?? asString(person.driverLicense),
},
{ label: PR.phone, value: asString(person.phoneNumber) },
{ label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) },
{ label: PR.birthDate, value: formatBirthDate(person.driverBirthday) },
{ label: PR.licenseNumber, value: asString(person.driverLicense) },
]);
}
function buildPartyVehicleSection(
title: string,
party: ReportParty | null | undefined,
claimVehicle?: Record<string, unknown>,
): InsurerFileReportSection | undefined {
if (!party && !claimVehicle) return undefined;
return buildSection(title, [
...flattenObject(claimVehicle, "claim.vehicle"),
...flattenObject(party?.vehicle, "party.vehicle"),
]);
}
function buildPartyStatementSection(
title: string,
party: ReportParty | null | undefined,
damagedParty: ReportParty | null,
guiltyParty: ReportParty | null,
): InsurerFileReportSection | undefined {
if (!party) return undefined;
const statement = (party.statement ?? {}) as ReportRecord;
const kind = partyKindLabel(party, damagedParty, guiltyParty);
return buildSection(title, [
{
label: PR.partyRole,
value: partyRoleLabel(getPartyRole(party)),
},
{
label: PR.name,
value: firstDefined(party.person?.fullName),
},
{
label: PR.admitsGuilt,
value:
kind === "damaged"
? undefined
: statementBoolean(statement.admitsGuilt),
},
{
label: PR.claimsDamage,
value:
kind === "guilty"
? undefined
: statementBoolean(statement.claimsDamage),
},
{
label: PR.acceptsExpertOpinion,
value: statementBoolean(statement.acceptsExpertOpinion),
},
{
label: PR.partyDescription,
value: asString(statement.description),
},
]);
}
function buildCaseTimelineSection(
overview?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
): InsurerFileReportSection {
const evaluationReply = resolveEvaluationReply(claim);
return buildSection(PR.timelineSection, [
{
label: PR.fileRegisteredAt,
value: formatDateTime(overview?.createdAt),
},
{
label: PR.evaluationRegisteredAt,
value: formatDateTime(evaluationReply?.submittedAt),
},
]);
}
function buildFanavaranCodesSection(
claim?: Record<string, unknown> | null,
): InsurerFileReportSection | undefined {
if (!claim) return undefined;
const sync = (claim.fanavaranSync as Record<string, unknown> | undefined) ?? {};
const baseClaim = (sync.baseClaim as Record<string, unknown> | undefined) ?? {};
const damageCase = (sync.damageCase as Record<string, unknown> | undefined) ?? {};
const expertise = (sync.expertise as Record<string, unknown> | undefined) ?? {};
return buildSection(PR.fanavaranSection, [
{
label: PR.fanavaranClaimNo,
value: firstDefined(claim.claimNo, baseClaim.claimNo),
},
{
label: PR.fanavaranClaimId,
value: firstDefined(claim.claimId, baseClaim.claimId),
},
{
label: PR.fanavaranDamageCaseId,
value: firstDefined(claim.dmgCaseId, damageCase.dmgCaseId, expertise.dmgCaseId),
},
{
label: PR.fanavaranExpertiseId,
value: firstDefined(claim.expertiseId, expertise.expertiseId),
},
{
label: PR.fanavaranPolicyId,
value: firstDefined(baseClaim.policyId),
},
{
label: PR.fanavaranDriverId,
value: firstDefined(baseClaim.driverId),
},
{
label: PR.fanavaranVehicleKindId,
value: firstDefined(baseClaim.vehicleKindId),
},
{
label: PR.fanavaranInsuranceCorpId,
value: firstDefined(baseClaim.insuranceCorpId),
},
]);
}
function buildEvaluationSection(
claim?: Record<string, unknown> | null,
): InsurerFileReportSection | undefined {
if (!claim) return undefined;
const reply = resolveEvaluationReply(claim);
const actorDetail = reply?.actorDetail as { actorName?: string } | undefined;
const snapshotName = expertNameFromSnapshot(
reply?.expertProfileSnapshot as
| { firstName?: string; lastName?: string }
| undefined,
);
return buildSection(PR.evaluationSection, [
{
label: PR.evaluationResult,
value: persianStatus(claim.claimStatus),
},
{
label: PR.evaluationExpert,
value: actorDetail?.actorName ?? snapshotName,
},
{
label: PR.evaluationSubmittedAt,
value: formatDateTime(reply?.submittedAt),
},
{
label: PR.evaluationResponse,
value: asString(reply?.description),
},
]);
}
function buildAccidentReportSection(
blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null,
damagedParty?: ReportParty | null,
): InsurerFileReportSection {
const statement = damagedParty?.statement as ReportRecord | undefined;
const location = damagedParty?.location;
const snapshotAccident = (
claim?.snapshot as { accident?: Record<string, unknown> } | undefined
)?.accident;
const blameDecision = (
blame?.expert as Record<string, unknown> | undefined
)?.decision as Record<string, unknown> | undefined;
const decisionFields = blameDecision?.fields as Record<string, unknown> | undefined;
return buildSection(PR.accidentSection, [
{
label: PR.accidentDate,
value:
asString(statement?.accidentDate) ??
asString(snapshotAccident?.date) ??
asString(blame?.createdAtFormatted) ??
formatDateTime(blame?.createdAt),
},
{
label: PR.accidentTime,
value: asString(statement?.accidentTime) ?? asString(snapshotAccident?.time),
},
{
label: PR.experts,
value: collectExpertNames(blame, claim),
},
{
label: PR.location,
value:
location?.lat != null && location?.lon != null
? `${location.lat}، ${location.lon}`
: undefined,
},
{
label: PR.weather,
value:
asString(statement?.weatherCondition) ??
asString(snapshotAccident?.weatherCondition),
},
{
label: PR.road,
value:
asString(statement?.roadCondition) ??
asString(snapshotAccident?.roadCondition),
},
{
label: PR.light,
value:
asString(statement?.lightCondition) ??
asString(snapshotAccident?.lightCondition),
},
{
label: PR.blameStatus,
value: persianStatus(blame?.blameStatus),
},
{
label: PR.claimStatus,
value: persianStatus(claim?.claimStatus),
},
{ label: PR.expertDecision, value: asString(blameDecision?.description) },
{
label: PR.accidentWay,
value: asString(
(decisionFields?.accidentWay as { label?: string } | undefined)?.label ??
(
snapshotAccident?.classification as {
accidentWay?: { label?: string };
}
)?.accidentWay?.label,
),
},
{
label: PR.accidentReason,
value: asString(
(decisionFields?.accidentReason as { label?: string } | undefined)?.label ??
(
snapshotAccident?.classification as {
accidentReason?: { label?: string };
}
)?.accidentReason?.label,
),
},
{
label: PR.accidentType,
value: asString(
(decisionFields?.accidentType as { label?: string } | undefined)?.label ??
(
snapshotAccident?.classification as {
accidentType?: { label?: string };
}
)?.accidentType?.label,
),
},
{
label: PR.partyDescription,
value: asString(statement?.description),
},
]);
}
export function buildInsurerFileReport(file: {
overview?: Record<string, unknown>;
blame?: Record<string, unknown>;
claim?: Record<string, unknown>;
}): InsurerFileReportViewModel {
const overview = file.overview ?? {};
const claim = file.claim ?? null;
const blame = file.blame ?? null;
const blameContext = resolveReportBlameContext(blame, claim);
const damagedParty = blameContext
? (resolveDamagedPartyRow(blameContext as any) as ReportParty | null)
: null;
const guiltyParty = blameContext
? (resolveClaimOwnerParty(blameContext as any) as ReportParty | null)
: null;
const isCarBody =
String((blameContext?.type as string | undefined) ?? "") === "CAR_BODY";
const includeGuiltySections = !!guiltyParty && !(isCarBody && sameParty(damagedParty, guiltyParty));
const claimVehicle = claim?.vehicle as Record<string, unknown> | undefined;
const sections = filterEmptySections([
buildCaseTimelineSection(overview, claim),
buildPartyOwnerSection(PR.ownerSection, damagedParty, {
claim,
useClaimOwnerFallback: true,
includeSheba: true,
}),
includeGuiltySections
? buildPartyOwnerSection(PR.guiltyOwnerSection, guiltyParty)
: undefined,
buildDriverSection(damagedParty, blameContext),
buildSection(
PR.damagedThirdPartyInsuranceSection,
buildThirdPartyInsuranceFields(damagedParty, blameContext, claim),
),
buildSection(
PR.damagedCarBodyInsuranceSection,
buildCarBodyInsuranceFields(damagedParty, blameContext, claim),
),
includeGuiltySections
? buildSection(
PR.guiltyThirdPartyInsuranceSection,
buildThirdPartyInsuranceFields(guiltyParty, blameContext, claim),
)
: undefined,
includeGuiltySections
? buildSection(
PR.guiltyCarBodyInsuranceSection,
buildCarBodyInsuranceFields(guiltyParty, blameContext, claim),
)
: undefined,
buildPartyVehicleSection(PR.damagedVehicleSection, damagedParty, claimVehicle),
includeGuiltySections
? buildPartyVehicleSection(PR.guiltyVehicleSection, guiltyParty)
: undefined,
buildPartyStatementSection(
PR.damagedStatementSection,
damagedParty,
damagedParty,
guiltyParty,
),
includeGuiltySections
? buildPartyStatementSection(
PR.guiltyStatementSection,
guiltyParty,
damagedParty,
guiltyParty,
)
: undefined,
buildFanavaranCodesSection(claim),
buildEvaluationSection(claim),
buildAccidentReportSection(blameContext, claim, damagedParty),
]);
return {
title: PR.reportTitle,
publicId: asString(overview.publicId) ?? PR.empty,
requestNo: asString(overview.requestNo),
sections,
};
}

View File

@@ -0,0 +1,61 @@
import {
Controller,
Get,
HttpException,
InternalServerErrorException,
Param,
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { CaseExpertReportService } from "./case-expert-report.service";
import { InsurerFileReportViewModel } from "./case-expert-report.types";
@ApiTags("expert-insurer-panel")
@Controller("expert-insurer")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.COMPANY)
export class CaseExpertReportInsurerController {
constructor(
private readonly caseExpertReportService: CaseExpertReportService,
) {}
@Get("files/:publicId/report")
@ApiOperation({
summary: "Get insurer file report data",
description:
"Returns structured insurer PDF data for the given publicId (blame + claim combined), including separated guilty/damaged insurance blocks, third-party/body policy details, Fanavaran codes, case/evaluation timestamps, and the evaluation result with expert response.",
})
@ApiParam({ name: "publicId" })
@ApiResponse({ status: 200, description: "Report data" })
@ApiResponse({ status: 404, description: "File not found for this publicId" })
async getInsurerReport(
@CurrentUser() insurer: { clientKey?: string },
@Param("publicId") publicId: string,
): Promise<InsurerFileReportViewModel> {
try {
return await this.caseExpertReportService.generateForInsurer(
publicId,
insurer,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to retrieve insurer file report data",
);
}
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { ExpertInsurerModule } from "src/expert-insurer/expert-insurer.module";
import { CaseExpertReportInsurerController } from "./case-expert-report.controller";
import { CaseExpertReportService } from "./case-expert-report.service";
@Module({
imports: [ExpertInsurerModule],
controllers: [CaseExpertReportInsurerController],
providers: [CaseExpertReportService],
exports: [CaseExpertReportService],
})
export class CaseExpertReportModule {}

View File

@@ -0,0 +1,27 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { ExpertInsurerService } from "src/expert-insurer/expert-insurer.service";
import { buildInsurerFileReport } from "./case-expert-report.builder";
import { InsurerFileReportViewModel } from "./case-expert-report.types";
@Injectable()
export class CaseExpertReportService {
constructor(
private readonly expertInsurerService: ExpertInsurerService,
) {}
async generateForInsurer(
publicId: string,
actor: { clientKey?: string },
): Promise<InsurerFileReportViewModel> {
const clientKey = actor?.clientKey;
if (!clientKey) {
throw new NotFoundException("Insurer context not found");
}
const file = await this.expertInsurerService.retrieveFileDetailsByPublicId(
clientKey,
publicId,
);
return buildInsurerFileReport(file);
}
}

View File

@@ -0,0 +1,16 @@
export type InsurerFileReportField = {
label: string;
value?: string | number | null;
};
export type InsurerFileReportSection = {
title: string;
fields: InsurerFileReportField[];
};
export type InsurerFileReportViewModel = {
title: string;
publicId: string;
requestNo?: string;
sections: InsurerFileReportSection[];
};

View File

@@ -0,0 +1,233 @@
export const PR = {
reportTitle: "گزارش پرونده بیمه گر",
publicId: "شناسه عمومی",
requestNo: "شماره درخواست",
empty: "-",
ownerSection: "مالک خودروی زیان دیده",
guiltyOwnerSection: "مالک خودروی مقصر",
driverSection: "راننده خودروی زیان دیده",
damagedThirdPartyInsuranceSection: "بیمه شخص ثالث زیان‌دیده",
damagedCarBodyInsuranceSection: "بیمه بدنه زیان‌دیده",
guiltyThirdPartyInsuranceSection: "بیمه شخص ثالث مقصر",
guiltyCarBodyInsuranceSection: "بیمه بدنه مقصر",
damagedVehicleSection: "اطلاعات خودروی زیان‌دیده",
guiltyVehicleSection: "اطلاعات خودروی مقصر",
vehicleSection: "اطلاعات خودرو",
damagedStatementSection: "اظهارات و اقرار زیان‌دیده",
guiltyStatementSection: "اظهارات و اقرار مقصر",
timelineSection: "زمان‌بندی پرونده",
fanavaranSection: "کدهای فناوران",
evaluationSection: "نتیجه ارزیابی",
accidentSection: "گزارش حادثه",
name: "نام",
phone: "شماره تلفن",
nationalCode: "کد ملی",
birthDate: "تاریخ تولد",
sheba: "شماره شبا",
licenseType: "نوع گواهینامه",
licenseDate: "تاریخ گواهینامه",
licenseNumber: "شماره گواهینامه",
driverLicense: "گواهینامه راننده",
insuranceCompany: "شرکت بیمه",
policyNumber: "شماره بیمه‌نامه",
policyStartDate: "تاریخ شروع بیمه‌نامه",
policyEndDate: "تاریخ پایان بیمه‌نامه",
financialCeiling: "سقف تعهد مالی",
coverages: "پوشش‌ها",
fileRegisteredAt: "تاریخ و ساعت ثبت پرونده",
evaluationRegisteredAt: "تاریخ و ساعت ثبت نتیجه ارزیابی",
fanavaranClaimNo: "شماره پرونده فناوران",
fanavaranClaimId: "کد پرونده فناوران",
fanavaranDamageCaseId: "کد کیس خسارت فناوران",
fanavaranExpertiseId: "کد کارشناسی فناوران",
fanavaranPolicyId: "کد بیمه‌نامه فناوران",
fanavaranDriverId: "کد راننده فناوران",
fanavaranVehicleKindId: "کد نوع خودرو فناوران",
fanavaranInsuranceCorpId: "کد شرکت بیمه فناوران",
evaluationResult: "نتیجه ارزیابی",
evaluationExpert: "کارشناس ارزیاب",
evaluationSubmittedAt: "تاریخ و ساعت ثبت ارزیابی",
evaluationResponse: "پاسخ / توضیحات کارشناس",
admitsGuilt: "اقرار به تقصیر",
claimsDamage: "ادعای خسارت",
acceptsExpertOpinion: "پذیرش نظر کارشناس",
partyRole: "نقش طرف",
data: "اطلاعات",
date: "تاریخ",
time: "زمان",
accidentDate: "تاریخ حادثه",
accidentTime: "ساعت حادثه",
experts: "کارشناس(ان)",
location: "موقعیت (عرض و طول جغرافیایی)",
weather: "وضعیت آب و هوا",
road: "وضعیت جاده",
light: "وضعیت نور",
blameStatus: "وضعیت مقصر",
claimStatus: "وضعیت خسارت",
expertDecision: "نظر کارشناس مقصر",
accidentWay: "نحوه برخورد",
accidentReason: "علت حادثه",
accidentType: "نوع حادثه",
damageExpertDate: "تاریخ ارزیابی کارشناس خسارت",
damageExpertNotes: "توضیحات کارشناس خسارت",
partyDescription: "توضیحات طرف",
} as const;
const KEY_LABELS: Record<string, string> = {
policyNumber: "شماره بیمه‌نامه",
company: "شرکت بیمه",
insurerCompany: "شرکت بیمه‌گر",
startDate: "تاریخ شروع",
endDate: "تاریخ پایان",
financialCeiling: "سقف مالی",
coverages: "پوشش‌ها",
plateId: "پلاک",
name: "نام",
model: "مدل",
type: "نوع",
carName: "نام خودرو",
carModel: "مدل خودرو",
carType: "نوع خودرو",
isNew: "خودرو نو",
isNewCar: "خودرو نو",
leftDigits: "دو رقم چپ پلاک",
centerAlphabet: "حرف پلاک",
centerDigits: "سه رقم وسط پلاک",
ir: "کد ایران",
plate: "پلاک",
CompanyName: "نام شرکت",
PolicyNumber: "شماره بیمه‌نامه",
PolicyStartDate: "تاریخ شروع بیمه",
PolicyEndDate: "تاریخ پایان بیمه",
LicenseType: "نوع گواهینامه",
licenseType: "نوع گواهینامه",
IssueDate: "تاریخ صدور",
issueDate: "تاریخ صدور",
ExpireDate: "تاریخ انقضا",
expireDate: "تاریخ انقضا",
party: "طرف",
insurance: "بیمه",
carBodyInsurance: "بیمه بدنه",
inquiry: "استعلام",
thirdParty: "شخص ثالث",
carBody: "بدنه",
claim: "خسارت",
vehicle: "خودرو",
blame: "مقصر",
items: "موارد",
value: "مقدار",
mapped: "نتیجه استعلام",
has: "موجود",
updatedAt: "به‌روزرسانی",
source: "منبع",
PrntPlcyCmpDocNo: "شماره سند شرکت",
MapTypNam: "نام نوع خودرو",
MtrNum: "شماره موتور",
ShsNum: "شماره شاسی",
vin: "VIN",
VinNumberField: "VIN",
DisFnYrPrcnt: "درصد تخفیف مالی",
DisLfYrPrcnt: "درصد تخفیف جانی",
DisPrsnYrPrcnt: "درصد تخفیف شخص ثالث",
MapVehicleSystemName: "سیستم خودرو",
LfCvrCptl: "سرمایه پوشش جانی",
FnCvrCptl: "سرمایه پوشش مالی",
PrsnCvrCptl: "سرمایه پوشش شخص ثالث",
PersonCvrCptl: "سرمایه پوشش شخص",
LifeCvrCptl: "سرمایه پوشش حیات",
FinancialCvrCptl: "سرمایه پوشش مالی",
VehicleSystemCode: "کد سیستم خودرو",
CarGroupCode: "کد گروه خودرو",
CylCnt: "تعداد سیلندر",
LastCompanyDocumentNumber: "شماره سند آخرین شرکت",
UsageCode: "کد کاربری",
MapUsageCode: "کد کاربری نگاشت‌شده",
MapUsageName: "نام کاربری",
Plk1: "دو رقم چپ پلاک",
Plk2: "حرف پلاک",
Plk3: "سه رقم وسط پلاک",
PlkSrl: "کد ایران پلاک",
SystemField: "سیستم",
TypeField: "تیپ",
UsageField: "کاربری",
MainColorField: "رنگ اصلی",
SecondColorField: "رنگ فرعی",
ModelField: "مدل",
CapacityField: "ظرفیت",
CacityField: "ظرفیت",
CylinderNumberField: "تعداد سیلندر",
EngineNumberField: "شماره موتور",
ChassisNumberField: "شماره شاسی",
InstallDateField: "تاریخ نصب",
AxelNumberField: "تعداد محور",
WheelNumberField: "تعداد چرخ",
CompanyCode: "کد شرکت",
SatrtDate: "تاریخ شروع",
EndDate: "تاریخ پایان",
PolicyHealthLossCount: "تعداد خسارت جانی",
PolicyFinancialLossCount: "تعداد خسارت مالی",
PolicyPersonLossCount: "تعداد خسارت شخص ثالث",
Tonage: "تناژ",
ThirdPolicyCode: "کد بیمه‌نامه ثالث",
SystemCodeCii: "کد سیستم",
SystemNameCii: "نام سیستم",
TypeCodeCii: "کد نوع",
TypeNameCii: "نام نوع",
UsageNameCii: "نام کاربری",
UsageCodeCii: "کد کاربری",
ModelCii: "مدل",
StatusTypeCode: "کد وضعیت",
label_fa: "برچسب فارسی",
catalogKey: "کلید کاتالوگ",
};
const STATUS_LABELS: Record<string, string> = {
AGREED: "توافق",
DISAGREEMENT: "اختلاف نظر",
UNKNOWN: "نامشخص",
APPROVED: "تأیید شده",
REJECTED: "رد شده",
NEEDS_REVISION: "نیاز به بازبینی",
UNDER_REVIEW: "در حال بررسی",
PENDING: "در انتظار",
true: "بله",
false: "خیر",
};
/** Turn `party.insurance.policyNumber` into a Persian label. */
export function persianFieldPath(path: string): string {
if (!path) return PR.data;
const parts = path.split(".").filter(Boolean);
const last = parts[parts.length - 1] ?? path;
const translatedLast = KEY_LABELS[last] ?? last;
const inquiryRoot = parts.find(
(p) => p === "thirdParty" || p === "carBody" || p === "mapped",
);
if (inquiryRoot === "thirdParty") {
return `بیمه شخص ثالث / ${translatedLast}`;
}
if (inquiryRoot === "carBody") {
return `بیمه بدنه / ${translatedLast}`;
}
if (parts[0] === "party" && parts[1] === "insurance") {
return `بیمه / ${translatedLast}`;
}
if (parts[0] === "claim" && parts[1] === "vehicle") {
return `خودرو / ${translatedLast}`;
}
if (parts[0] === "party" && parts[1] === "vehicle") {
return `خودرو / ${translatedLast}`;
}
if (parts.length === 1) return translatedLast;
const translated = parts.map((part) => KEY_LABELS[part] ?? part);
return translated.join(" / ");
}
export function persianStatus(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined;
const key = String(value);
return STATUS_LABELS[key] ?? key;
}

View File

@@ -0,0 +1,59 @@
import { ForbiddenException } from "@nestjs/common";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
describe("V2 claim-detail access for split file roles", () => {
const makerId = "maker-id";
const reviewerId = "reviewer-id";
const createService = (blame: Record<string, unknown>) => {
const service = Object.create(
ClaimRequestManagementService.prototype,
) as ClaimRequestManagementService;
(service as any).blameRequestDbService = {
findById: jest.fn().mockResolvedValue(blame),
};
return service;
};
const claim = { blameRequestId: "blame-id" };
it("allows the FileMaker who created a completed V4/V5 file", async () => {
const service = createService({
isMadeByFileMaker: true,
expertInitiated: true,
creationMethod: "IN_PERSON",
initiatedByFieldExpertId: makerId,
});
await expect(
(service as any).assertActorCanViewClaimV2(claim, makerId, {
sub: makerId,
role: RoleEnum.FILE_MAKER,
}),
).resolves.toBeUndefined();
});
it("allows only the assigned FileReviewer", async () => {
const service = createService({
isMadeByFileMaker: true,
expertInitiated: true,
creationMethod: "IN_PERSON",
assignedFileReviewerId: reviewerId,
});
await expect(
(service as any).assertActorCanViewClaimV2(claim, reviewerId, {
sub: reviewerId,
role: RoleEnum.FILE_REVIEWER,
}),
).resolves.toBeUndefined();
await expect(
(service as any).assertActorCanViewClaimV2(claim, "other-reviewer", {
sub: "other-reviewer",
role: RoleEnum.FILE_REVIEWER,
}),
).rejects.toBeInstanceOf(ForbiddenException);
});
});

View File

@@ -22,14 +22,16 @@ import {
ApiParam,
ApiQuery,
ApiTags,
ApiOperation,
ApiExcludeController,
} from "@nestjs/swagger";
import { diskStorage } from "multer";
import { GlobalGuard } from "src/auth/guards/global.guard";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
import { CarDamagePartDto, OtherCarDamagePartDto } from "./dto/car-part.dto";
@@ -38,8 +40,9 @@ import { UserObjectionDto } from "./dto/user-objection.dto";
import { InPersonVisitDto } from "./dto/in-person-visit.dto";
import { UserRatingDto } from "./dto/user-rating.dto";
@ApiExcludeController()
@Controller("claim-request-management")
@ApiTags("claim-request-management")
// @ApiTags("claim-request-management")
@Roles(RoleEnum.USER, RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
@UseGuards(ClaimAccessGuard, RolesGuard)
@ApiBearerAuth()
@@ -48,7 +51,8 @@ export class ClaimRequestManagementController {
private readonly claimRequestManagementService: ClaimRequestManagementService,
) {}
@ApiParam({ name: "blameId" })
// @ApiParam({ name: "blameId" })
// @ApiOperation({ deprecated: true })
@Post("/:blameId")
async createClaimRequest(
@Param("blameId") requestId: string,
@@ -61,9 +65,10 @@ export class ClaimRequestManagementController {
);
}
@ApiBody({ type: CarDamagePartDto })
// @ApiBody({ type: CarDamagePartDto })
// @ApiOperation({ deprecated: true })
@Patch("/car-part-damage/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
// @ApiParam({ name: "claimRequestID" })
async carPartDamage(
@Param("claimRequestID") requestId: string,
@Body() body: CarDamagePartDto,
@@ -76,6 +81,7 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Get("/car-other-part")
async getCarOtherParts() {
const carOtherPart = await readFile(
@@ -85,12 +91,12 @@ export class ClaimRequestManagementController {
return carOtherPart;
}
@ApiBody({ type: OtherCarDamagePartDto })
@ApiParam({ name: "claimRequestID" })
// @ApiBody({ type: OtherCarDamagePartDto })
// @ApiParam({ name: "claimRequestID" })
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024,
fileSize: DEFAULT_MEDIA_MAX_BYTES,
},
storage: diskStorage({
destination: "./files/car-green-cards",
@@ -104,6 +110,7 @@ export class ClaimRequestManagementController {
}),
)
@ApiConsumes("multipart/form-data")
// @ApiOperation({ deprecated: true })
@Patch("/car-other-part-damage/:claimRequestID")
async carOtherPartDamage(
@Param("claimRequestID") requestId: string,
@@ -119,35 +126,35 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Get("required-documents-status/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
async getRequiredDocumentsStatus(
@Param("claimRequestID") requestId: string,
) {
// @ApiParam({ name: "claimRequestID" })
async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
requestId,
);
}
// @ApiOperation({ deprecated: true })
@Get("car-part-image-required/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
// @ApiParam({ name: "claimRequestID" })
async getImageRequired(@Param("claimRequestID") requestId) {
return await this.claimRequestManagementService.getImageRequiredList(
requestId,
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
// @ApiBody({
// schema: {
// type: "object",
// properties: {
// file: { type: "string", format: "binary" },
// },
// },
// })
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 10 * 1024 * 1024 },
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/claim-required-documents/",
filename: (req, file, callback) => {
@@ -164,13 +171,14 @@ export class ClaimRequestManagementController {
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
@ApiQuery({
name: "documentType",
enum: ClaimRequiredDocumentType,
description: "Type of required document to upload",
})
// @ApiConsumes("multipart/form-data")
// // @ApiParam({ name: "claimRequestID" })
// @ApiQuery({
// name: "documentType",
// enum: ClaimRequiredDocumentType,
// description: "Type of required document to upload",
// })
// @ApiOperation({ deprecated: true })
@Patch("upload-required-document/:claimRequestID")
async uploadRequiredDocument(
@Param("claimRequestID") requestId: string,
@@ -189,17 +197,17 @@ export class ClaimRequestManagementController {
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
// @ApiBody({
// schema: {
// type: "object",
// properties: {
// file: { type: "string", format: "binary" },
// },
// },
// })
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 10 * 1024 * 1024 },
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/car-parts/",
filename: (req, file, callback) => {
@@ -217,12 +225,13 @@ export class ClaimRequestManagementController {
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
@ApiParam({
name: "partId",
description: "The ID of the specific car part being photographed.",
})
// @ApiConsumes("multipart/form-data")
// @ApiParam({ name: "claimRequestID" })
// @ApiParam({
// name: "partId",
// description: "The ID of the specific car part being photographed.",
// })
// @ApiOperation({ deprecated: true })
@Patch("capture-car-part-damage/:claimRequestID/:partId")
async captureCarPartDamage(
@Param("partId") partId: string,
@@ -239,17 +248,17 @@ export class ClaimRequestManagementController {
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
// @ApiBody({
// schema: {
// type: "object",
// properties: {
// file: { type: "string", format: "binary" },
// },
// },
// })
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 50 * 1024 * 1024 },
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/car-capture-videos/",
filename: (req, file, callback) => {
@@ -261,8 +270,9 @@ export class ClaimRequestManagementController {
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
// @ApiConsumes("multipart/form-data")
// @ApiParam({ name: "claimRequestID" })
// @ApiOperation({ deprecated: true })
@Patch("car-capture/:claimRequestID")
async captureVideoCapture(
@Param("claimRequestID") requestId: string,
@@ -274,26 +284,26 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Get("requests/")
async getRequest(@CurrentUser() currentUser) {
return await this.claimRequestManagementService.myRequests(currentUser);
}
// @ApiOperation({ deprecated: true })
@Get("request/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
myRequests(
@Param("claimRequestId") requestId: string,
@CurrentUser() user,
) {
// @ApiParam({ name: "claimRequestId" })
myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
return this.claimRequestManagementService.requestDetails(requestId, user);
}
// @ApiOperation({ deprecated: true })
@Put("request/reply/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
// @ApiParam({ name: "claimRequestId" })
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024,
fileSize: DEFAULT_MEDIA_MAX_BYTES,
},
storage: diskStorage({
destination: "./files/claim-sign",
@@ -306,33 +316,34 @@ export class ClaimRequestManagementController {
}),
}),
)
@ApiBody({
type: UserCommentDto,
description: "if partId null , you can upload video capture",
})
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
// @ApiBody({
// type: UserCommentDto,
// description: "if partId null , you can upload video capture",
// })
// @ApiConsumes("multipart/form-data")
// @ApiParam({ name: "claimRequestId" })
async submitReply(
@Param("claimRequestId") requestId,
@Body() body,
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.submitUserReply(
requestId,
body,
file,
user,
);
// return await this.claimRequestManagementService.submitUserReply(
// requestId,
// body,
// file,
// user,
// );
}
// @ApiOperation({ deprecated: true })
@Put("request/resend/:claimRequestId/objection")
@ApiParam({ name: "claimRequestId" })
@ApiConsumes("application/json")
@ApiBody({
type: UserObjectionDto,
description: "Objection details with optional new parts",
})
// @ApiParam({ name: "claimRequestId" })
// @ApiConsumes("application/json")
// @ApiBody({
// type: UserObjectionDto,
// description: "Objection details with optional new parts",
// })
async handleUserObjection(
@Param("claimRequestId") claimRequestId: string,
@Body() userObjectionDto: UserObjectionDto,
@@ -343,24 +354,25 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Patch("request/resend/:claimRequestId")
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
@ApiQuery({ name: "fields", enum: ["resendDocuments", "resendCarParts"] })
@ApiQuery({ name: "partId", required: false })
@ApiQuery({ name: "documentName", required: false })
@ApiQuery({ name: "side", required: false })
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
// @ApiConsumes("multipart/form-data")
// @ApiParam({ name: "claimRequestId" })
// @ApiQuery({ name: "fields", enum: ["resendDocuments", "resendCarParts"] })
// @ApiQuery({ name: "partId", required: false })
// @ApiQuery({ name: "documentName", required: false })
// @ApiQuery({ name: "side", required: false })
// @ApiBody({
// schema: {
// type: "object",
// properties: {
// file: {
// type: "string",
// format: "binary",
// },
// },
// },
// })
@UseInterceptors(
FileInterceptor("file", {
storage: diskStorage({
@@ -372,7 +384,7 @@ export class ClaimRequestManagementController {
callback(null, filename);
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
}),
)
async uploadDocuments(
@@ -393,9 +405,10 @@ export class ClaimRequestManagementController {
* User satisfaction rating for a completed claim file.
* Only the damaged user (claim owner) can rate their claim after it is closed.
*/
// @ApiOperation({ deprecated: true })
@Put("request/:claimRequestId/user-rating")
@ApiParam({ name: "claimRequestId" })
@ApiBody({ type: UserRatingDto })
// @ApiParam({ name: "claimRequestId" })
// @ApiBody({ type: UserRatingDto })
async addUserRating(
@Param("claimRequestId") claimRequestId: string,
@Body() ratingDto: UserRatingDto,
@@ -408,21 +421,22 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
@ApiParam({ name: "partId" })
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
// @ApiConsumes("multipart/form-data")
// @ApiParam({ name: "claimRequestId" })
// @ApiParam({ name: "partId" })
// @ApiBody({
// schema: {
// type: "object",
// properties: {
// file: {
// type: "string",
// format: "binary",
// },
// },
// },
// })
@UseInterceptors(
FileInterceptor("file", {
storage: diskStorage({
@@ -433,7 +447,7 @@ export class ClaimRequestManagementController {
callback(null, filename);
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
}),
)
async uploadFactorForPart(
@@ -450,8 +464,9 @@ export class ClaimRequestManagementController {
);
}
@ApiBody({ type: InPersonVisitDto })
@ApiParam({ name: "id" })
// @ApiBody({ type: InPersonVisitDto })
// @ApiParam({ name: "id" })
// @ApiOperation({ deprecated: true })
@Patch(":id/visit")
async inPersonVisit(
@Param("id") requestId: string,
@@ -466,23 +481,25 @@ export class ClaimRequestManagementController {
);
}
// @ApiOperation({ deprecated: true })
@Get("branches/:insuranceId")
// @ApiParam({ name: "insuranceId" })
async insuranceBranches(@Param("insuranceId") insuranceId: string) {
return await this.claimRequestManagementService.retrieveInsuranceBranches(
insuranceId,
);
return await this.claimRequestManagementService.retrieveInsuranceBranches(insuranceId);
}
// @ApiOperation({ deprecated: true })
@Get("fanavaran-submit/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
// @ApiParam({ name: "claimRequestId" })
async fanavaranSubmit(@Param("claimRequestId") claimRequestId: string) {
return await this.claimRequestManagementService.fanavaranSubmit(
claimRequestId,
);
}
// @ApiOperation({ deprecated: true })
@Post("fanavaran-submit/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
// @ApiParam({ name: "claimRequestId" })
async submitToFanavaran(@Param("claimRequestId") claimRequestId: string) {
return await this.claimRequestManagementService.submitToFanavaran(
claimRequestId,

View File

@@ -1,16 +1,25 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { HttpModule } from "@nestjs/axios";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import { AiModule } from "src/ai/ai.module";
import { SandHubModule } from "src/sand-hub/sand-hub.module";
import { RequestManagementModule } from "src/request-management/request-management.module";
import { UsersModule } from "src/users/users.module";
import { ClaimRequestManagementController } from "./claim-request-management.controller";
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
import { RegistrarClaimV1Controller } from "./registrar-claim.v1.controller";
import { ExpertInitiatedClaimMirrorController } from "./expert-initiated-claim.mirror.controller";
import { RegistrarClaimMirrorController } from "./registrar-claim.mirror.controller";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
import { ClaimCase, ClaimCaseSchema } from "./entites/schema/claim-cases.schema";
import {
ClaimCase,
ClaimCaseSchema,
} from "./entites/schema/claim-cases.schema";
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
@@ -45,15 +54,30 @@ import { PublicIdModule } from "src/utils/public-id/public-id.module";
import { ClientModule } from "src/client/client.module";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { JwtModule } from "@nestjs/jwt";
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
@Module({
imports: [
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
FanavaranAuditModule,
FanavaranLookupModule,
PlateNormalizerModule,
PublicIdModule,
UsersModule,
RequestManagementModule,
AiModule,
SandHubModule,
ClientModule,
MediaPolicyModule,
SmsOrchestrationModule,
JwtModule.register({}),
MongooseModule.forFeature([
{ name: ClaimCase.name, schema: ClaimCaseSchema },
@@ -85,7 +109,13 @@ import { JwtModule } from "@nestjs/jwt";
ClaimRequiredDocumentDbService,
ClaimAccessGuard,
],
controllers: [ClaimRequestManagementController, ClaimRequestManagementV2Controller],
controllers: [
ClaimRequestManagementController,
ClaimRequestManagementV2Controller,
RegistrarClaimV1Controller,
ExpertInitiatedClaimMirrorController,
RegistrarClaimMirrorController,
],
exports: [
ClaimRequestManagementService,
ClaimRequestManagementDbService,
@@ -93,6 +123,7 @@ import { JwtModule } from "@nestjs/jwt";
DamageImageDbService,
VideoCaptureDbService,
ClaimRequiredDocumentDbService,
ClaimSignDbService,
],
})
export class ClaimRequestManagementModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -2,56 +2,104 @@ import {
Controller,
HttpException,
InternalServerErrorException,
BadRequestException,
Param,
Query,
Post,
Patch,
Put,
Body,
UseGuards,
Get,
UseInterceptors,
UploadedFile,
UploadedFiles,
} from "@nestjs/common";
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
import { FileInterceptor } from "@nestjs/platform-express";
import { readFile } from "node:fs/promises";
import {
ApiBearerAuth,
ApiParam,
ApiTags,
ApiOperation,
ApiResponse,
ApiBody,
ApiConsumes,
} from "@nestjs/swagger";
import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { extname } from "path";
import { extname } from "node:path";
import { Types } from "mongoose";
import { GlobalGuard } from "src/auth/guards/global.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { MediaPolicyService } from "src/media-policy/media-policy.service";
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto";
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
import {
OuterPartCatalogItemDto,
SelectOuterPartsV2Dto,
SelectOuterPartsV2ResponseDto,
} from "./dto/select-outer-parts-v2.dto";
import {
SelectOtherPartsV2Dto,
SelectOtherPartsV2ResponseDto,
} from "./dto/select-other-parts-v2.dto";
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
import {
UploadRequiredDocumentV2Dto,
UploadRequiredDocumentV2ResponseDto,
} from "./dto/upload-document-v2.dto";
import {
CapturePartV2Dto,
CapturePartV2ResponseDto,
VideoCaptureV2ResponseDto,
} from "./dto/capture-part-v2.dto";
import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-v2.dto";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto";
import { UserRatingDto } from "./dto/user-rating.dto";
@ApiTags("claim-request-management (v2)")
@Controller("v2/claim-request-management")
@ApiBearerAuth()
@UseGuards(GlobalGuard, RolesGuard)
@Roles(RoleEnum.USER)
@Roles(
RoleEnum.USER,
RoleEnum.FIELD_EXPERT,
RoleEnum.REGISTRAR,
RoleEnum.FILE_MAKER,
RoleEnum.FILE_REVIEWER,
)
export class ClaimRequestManagementV2Controller {
constructor(
private readonly claimRequestManagementService: ClaimRequestManagementService,
private readonly mediaPolicyService: MediaPolicyService,
) {}
@Get("requests")
@ApiOperation({
summary: "Get My Claims (V2)",
description: "Get list of all claim requests for the current user.",
description:
"Claims for the current user, or claims from blame files initiated by the current FIELD_EXPERT / REGISTRAR (LINK and IN_PERSON). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`.",
})
@ApiResponse({
status: 200,
description: "List of user claims",
type: GetMyClaimsV2ResponseDto,
})
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
async getMyClaims(
@CurrentUser() user: any,
@Query() query: ListQueryV2Dto,
): Promise<GetMyClaimsV2ResponseDto> {
try {
return await this.claimRequestManagementService.getMyClaimsV2(user.sub);
return await this.claimRequestManagementService.getMyClaimsV2(
user.sub,
user,
query,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
@@ -68,7 +116,8 @@ export class ClaimRequestManagementV2Controller {
})
@ApiOperation({
summary: "Get Claim Details (V2)",
description: "Get full details of a claim request. Only the claim owner can access.",
description:
"Returns the claim snapshot for an authorized **USER**, **FIELD_EXPERT**, **REGISTRAR**, **FILE_MAKER**, or assigned **FILE_REVIEWER**. Initiating experts/registrars see unmasked money fields; owners get `ownerGuidance`. Completed claims include Fanavaran `claimNo` / `claimId` when available.",
})
@ApiResponse({
status: 200,
@@ -91,6 +140,7 @@ export class ClaimRequestManagementV2Controller {
return await this.claimRequestManagementService.getClaimDetailsV2(
claimRequestId,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
@@ -100,6 +150,277 @@ export class ClaimRequestManagementV2Controller {
}
}
/**
* V2: Acknowledge a damage-expert resend that only contains instructions (no extra documents or part photos).
*/
@Post("request/:claimRequestId/expert-resend/acknowledge")
@ApiOperation({
summary: "Acknowledge expert resend (instructions only)",
description:
"Use when `workflow.currentStep` is USER_EXPERT_RESEND and the expert did not list any `resendDocuments` or `resendCarParts`. " +
"Returns the claim to WAITING_FOR_DAMAGE_EXPERT. If documents or parts were requested, upload them via the existing upload/capture endpoints instead.",
})
@ApiParam({ name: "claimRequestId" })
@ApiResponse({ status: 200, description: "Claim returned to expert queue" })
@ApiResponse({
status: 400,
description: "Resend requires uploads or wrong step",
})
async acknowledgeExpertResend(
@Param("claimRequestId") claimRequestId: string,
@CurrentUser() user: any,
) {
try {
return await this.claimRequestManagementService.acknowledgeExpertResendInstructionsV2(
claimRequestId,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to acknowledge expert resend",
);
}
}
/**
* V2: User objection after expert resend (same intent as v1 PUT …/request/resend/:id/objection).
* Accepts multipart/form-data so optional supporting invoices can be attached in the same request.
*/
@Put("request/:claimRequestId/objection")
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Submit user objection (V2)",
description:
"**Windows:** (1) **Insurer-review:** `ClaimCaseStatus` in **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, with `workflow.currentStep=INSURER_REVIEW` and no recorded **final** `evaluation.ownerInsurerApproval` — including **mixed** priced+factor gate (`NEEDS_REVISION` before priced-line signature for factors) or **final** totals (`claimStatus=APPROVED`). Not allowed while uploading factors (`OWNER_UPLOAD_FACTOR_DOCUMENTS`) or expert validation (`EXPERT_COST_EVALUATION`/`EXPERT_VALIDATING_REPAIR_FACTORS`).\n" +
"(2) **Legacy resend:** active expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`).\n\n" +
"`objectionParts` may only reference **priced** repair lines (`factorNeeded=false`). Factor-only lines cannot be disputed until they have expert pricing.\n\n" +
"After **`damageExpertReplyFinal`** exists (final reply following a prior objection), **no second objection** — owner uses **owner-insurer-approval/sign** to accept/reject and close the case.\n\n" +
"Stores `evaluation.objection`, clears partial/final owner approval fields, merges `newParts` into `damage.selectedParts`, returns case to `WAITING_FOR_DAMAGE_EXPERT`.\n\n" +
"**Invoices:** optionally attach up to 5 supporting documents (images/PDFs) as `invoices` file fields. Stored in `evaluation.objection.invoices[]` and visible to the reviewing expert.",
})
@ApiParam({
name: "claimRequestId",
description: "The claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiBody({
description:
"Objection payload as multipart form fields. `objectionParts` and `newParts` are JSON-encoded strings.",
schema: {
type: "object",
properties: {
objectionParts: {
type: "string",
description:
'JSON-encoded array of disputed priced parts. Example: `[{"partId":201,"reason":"Price too high"}]`',
},
newParts: {
type: "string",
description:
'JSON-encoded array of new parts to add. Example: `[{"partName":"سپر جلو","side":"front"}]`',
},
invoices: {
type: "array",
items: { type: "string", format: "binary" },
description: "Up to 5 supporting invoice or document files (image or PDF).",
},
},
},
})
@ApiResponse({ status: 200, description: "Objection stored" })
@ApiResponse({
status: 400,
description: "No active resend or empty payload",
})
@ApiResponse({ status: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Objection already submitted" })
@UseInterceptors(
FilesInterceptor("invoices", 5, {
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/claim-objection-invoices",
filename: (req, file, callback) => {
const unique = Date.now() + "-" + Math.round(Math.random() * 1e6);
const ex = extname(file.originalname);
callback(null, `objection-invoice-${unique}${ex}`);
},
}),
}),
)
async submitUserObjectionV2(
@Param("claimRequestId") claimRequestId: string,
@Body() body: UserObjectionV2Dto,
@CurrentUser() user: any,
@UploadedFiles() invoices?: Express.Multer.File[],
) {
for (const file of invoices ?? []) {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
}
try {
return await this.claimRequestManagementService.handleUserObjectionV2(
claimRequestId,
body,
user.sub,
user,
invoices,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to submit objection",
);
}
}
/**
* V2: User satisfaction rating after the claim case is completed (same intent as v1 PUT …/request/:id/user-rating).
*/
@Put("request/:claimRequestId/user-rating")
@ApiOperation({
summary: "Submit user satisfaction rating (V2)",
description:
"Only the claim owner (damaged party) may submit. Allowed when `status` is `COMPLETED`. " +
"Stores scores on `ClaimCase.userRating` (0–5). One submission per case.",
})
@ApiParam({
name: "claimRequestId",
description: "The claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiBody({ type: UserRatingDto })
@ApiResponse({ status: 200, description: "Rating saved" })
@ApiResponse({
status: 400,
description: "Claim not completed or invalid scores",
})
@ApiResponse({ status: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Rating already submitted" })
async addUserRatingV2(
@Param("claimRequestId") claimRequestId: string,
@Body() ratingDto: UserRatingDto,
@CurrentUser() user: any,
) {
try {
return await this.claimRequestManagementService.addUserRatingV2(
claimRequestId,
ratingDto,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to save rating",
);
}
}
/**
* V2–V5: owner signature used only as the priced-line gate for mixed-factor claims.
* The final accept/reject phase is retained for legacy rows only.
*/
@Put("request/:claimRequestId/owner-insurer-approval/sign")
@ApiParam({
name: "claimRequestId",
description: "Claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Sign priced lines before factor uploads (owner; final phase is legacy only)",
description:
"Multipart: `sign`, `agree`, `branchId`. Requires `ClaimCaseStatus` **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, and `workflow.currentStep=INSURER_REVIEW` (not during owner factor upload or `EXPERT_COST_EVALUATION`).\n\n" +
"**Phase A — Mixed reply, priced lines only:** `claimStatus=NEEDS_REVISION`, no `evaluation.ownerPricedPartsApproval` yet. `agree=true` records that signature and moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads; `agree=false` rejects the whole case (`REJECTED`).\n\n" +
"**Phase B — Legacy final phase only:** pre-existing rows with `claimStatus=APPROVED` may still be accepted or rejected through this endpoint. New V2–V5 claims complete after expert work (and V5 FileMaker approval) without a final owner signature.\n\n" +
"Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or legacy `FINAL_APPROVAL` for UI state.",
})
@ApiBody({
description: "Signature file, agreement, and branch",
schema: {
type: "object",
required: ["sign", "agree", "branchId"],
properties: {
sign: {
type: "string",
format: "binary",
description: "Signature image",
},
agree: {
type: "boolean",
description: "true to accept expert pricing and complete the claim",
},
branchId: {
type: "string",
description:
"Insurer branch id (must belong to the claim owner's insurer; if pricing lists branch options, must match one of them)",
example: "507f1f77bcf86cd799439011",
},
},
},
})
@ApiResponse({
status: 200,
description: "Signature stored; claim completed or rejected",
})
@ApiResponse({
status: 400,
description: "Wrong step/status or missing file",
})
@ApiResponse({ status: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Already signed" })
@UseInterceptors(
FileInterceptor("sign", {
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/claim-sign",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const base = file.originalname.split(/[.,\s-]/)[0] || "sign";
callback(null, `${base}-${unique}${ex}`);
},
}),
}),
)
async submitOwnerInsurerApprovalSignV2(
@Param("claimRequestId") claimRequestId: string,
@Body("agree") agree: string | boolean,
@Body("branchId") branchId: string,
@CurrentUser() user: any,
@UploadedFile() sign: Express.Multer.File,
) {
if (!Types.ObjectId.isValid(claimRequestId)) {
throw new BadRequestException("Invalid claim request id");
}
await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image");
const agreed =
typeof agree === "string"
? agree === "true" || agree === "1"
: Boolean(agree);
try {
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
claimRequestId,
agreed,
typeof branchId === "string" ? branchId : "",
sign,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to submit signature",
);
}
}
@Post("create-from-blame/:blameRequestId")
@ApiParam({
name: "blameRequestId",
@@ -127,6 +448,64 @@ export class ClaimRequestManagementV2Controller {
/**
* V2 API: Select damaged outer car parts (Step 2 of claim workflow)
*/
@Get("outer-parts-catalog")
@ApiOperation({
summary: "Get outer parts catalog (V2)",
description:
"Returns the Fanavaran car-components list. All vehicle types share the same catalog.",
})
@ApiResponse({
status: 200,
description: "Outer parts catalog",
type: [OuterPartCatalogItemDto],
})
async getOuterPartsCatalog(): Promise<OuterPartCatalogItemDto[]> {
return await this.claimRequestManagementService.getOuterPartsCatalogV2();
}
@Get("branches/:insuranceId")
@ApiOperation({
summary: "Get insurer branches (V2)",
description:
"Returns branch list for a given insurer/client id so frontend can render branch options (name/code/address/city/state) and submit selected branchId in daghi part options.",
})
@ApiParam({
name: "insuranceId",
description: "Insurer client id (MongoDB ObjectId)",
example: "60d5ec49e7b2f8001c8e4d2a",
})
@ApiResponse({
status: 200,
description: "List of branches for insurer",
})
async getInsuranceBranchesV2(@Param("insuranceId") insuranceId: string) {
return await this.claimRequestManagementService.retrieveInsuranceBranches(
insuranceId,
);
}
@Get("car-other-part")
@ApiOperation({
summary: "Get other (non-body) parts catalog",
description:
"Returns legacy other-parts catalog used by frontend. Response is parsed JSON.",
})
@ApiResponse({
status: 200,
description: "Other parts catalog",
})
async getCarOtherPartsV2() {
const raw = await readFile(
`${process.cwd()}/src/static/car-part.json`,
"utf-8",
);
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
@Patch("select-outer-parts/:claimRequestId")
@ApiOperation({
summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
@@ -161,41 +540,34 @@ export class ClaimRequestManagementV2Controller {
})
@ApiBody({
type: SelectOuterPartsV2Dto,
description: "Array of selected damaged outer parts",
description: "Selected vehicle type + selected outer part IDs from catalog",
examples: {
example1: {
summary: "Minor front damage",
summary: "Sedan - minor front damage",
value: {
selectedParts: ["hood", "front_bumper", "front_right_fender"],
carType: "sedan",
selectedPartIds: [19, 21, 16],
},
},
example2: {
summary: "Side impact damage",
summary: "SUV - left side impact",
value: {
selectedParts: [
"front_left_door",
"rear_left_door",
"front_left_fender",
"rear_left_fender",
],
carType: "suv",
selectedPartIds: [102, 103, 104, 107],
},
},
example3: {
summary: "Rear-end collision",
summary: "Hatchback - rear-end collision",
value: {
selectedParts: ["rear_bumper", "trunk", "rear_right_fender"],
carType: "hatchback",
selectedPartIds: [225, 226, 210],
},
},
example4: {
summary: "Multiple damage areas",
summary: "Pickup - two sides + roof",
value: {
selectedParts: [
"hood",
"front_bumper",
"front_right_door",
"rear_bumper",
"trunk",
],
carType: "pickup",
selectedPartIds: [319, 312, 330],
},
},
},
@@ -231,13 +603,12 @@ export class ClaimRequestManagementV2Controller {
claimRequestId,
body,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to select outer parts",
error instanceof Error ? error.message : "Failed to select outer parts",
);
}
}
@@ -252,13 +623,14 @@ export class ClaimRequestManagementV2Controller {
**Workflow Step:** SELECT_OTHER_PARTS (Step 3 of Claim)
**Purpose:** User selects non-body damaged parts and provides bank information for payment.
Optional: upload car green card file in the same step.
**Validations:**
- Claim must exist
- User must be the claim owner (damaged party from blame case)
- Current workflow step must be SELECT_OTHER_PARTS
- Bank information must not have been submitted previously
- Sheba number must be exactly 24 digits
- Sheba (sheba) accepted as IR + 24 digits or only 24 digits
- National code must be exactly 10 digits
**Valid Other Parts (Optional):**
@@ -267,8 +639,8 @@ export class ClaimRequestManagementV2Controller {
- headlight, taillight, mirror, glass
**After Success:**
- Workflow moves to: UPLOAD_REQUIRED_DOCUMENTS (Step 4)
- User can proceed to upload required documents
- Workflow moves to: CAPTURE_PART_DAMAGES (Step 4)
- User captures car angles and damaged-part photos, then uploads required documents (Step 5)
`,
})
@ApiParam({
@@ -276,42 +648,38 @@ export class ClaimRequestManagementV2Controller {
description: "The claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiConsumes("multipart/form-data")
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/claim-required-document",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
callback(null, `other-parts-${unique}${ex}`);
},
}),
}),
)
@ApiBody({
type: SelectOtherPartsV2Dto,
description: "Other parts selection and bank information",
examples: {
example1: {
summary: "Only bank info (no other parts damaged)",
value: {
otherParts: [],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example2: {
summary: "Engine and suspension damage",
value: {
otherParts: ["engine", "suspension"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example3: {
summary: "Multiple systems damaged",
value: {
otherParts: ["engine", "brake_system", "electrical", "headlight"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example4: {
summary: "Lighting and glass damage",
value: {
otherParts: ["headlight", "taillight", "mirror", "glass"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
description:
"Other parts + bank information. Use `sheba` and `nationalCodeOfInsurer` like THIRD_PARTY flow. Optional file can be uploaded as car green card.",
schema: {
type: "object",
properties: {
otherParts: {
oneOf: [
{ type: "array", items: { type: "string" } },
{ type: "string", description: "JSON string array for multipart" },
],
example: ["engine", "suspension"],
},
sheba: { type: "string", example: "IR123456789012345678901234" },
nationalCodeOfInsurer: { type: "string", example: "1234567890" },
file: { type: "string", format: "binary" },
},
required: ["sheba", "nationalCodeOfInsurer"],
},
})
@ApiResponse({
@@ -339,19 +707,22 @@ export class ClaimRequestManagementV2Controller {
@Param("claimRequestId") claimRequestId: string,
@Body() body: SelectOtherPartsV2Dto,
@CurrentUser() user: any,
@UploadedFile() file?: Express.Multer.File,
): Promise<SelectOtherPartsV2ResponseDto> {
// Green-card photo is optional here — the helper no-ops on missing file.
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
try {
return await this.claimRequestManagementService.selectOtherPartsV2(
claimRequestId,
body,
user.sub,
user,
file,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to select other parts",
error instanceof Error ? error.message : "Failed to select other parts",
);
}
}
@@ -364,11 +735,13 @@ export class ClaimRequestManagementV2Controller {
summary: "Get Capture Requirements (V2)",
description: `
**Get list of what needs to be captured:**
- Required documents (13 items)
- Required documents (10 remaining at the documents step for third-party; 3 damaged-party items should be uploaded during capture — see \`preferUploadDuringCapture\` on each item)
- Car angles (4 items: front, back, left, right)
- Damaged parts (based on selected outer parts)
Returns status of each item (uploaded/captured or not).
**V2 order (enforced by API):** During \`CAPTURE_PART_DAMAGES\`, (1) all damaged-part photos, (2) four car angles, (3) chassis/engine/metal-plate via upload-document, then walk-around video. Remaining documents in \`UPLOAD_REQUIRED_DOCUMENTS\`. Use \`captureSequencePhase\` / \`captureSequenceHint\` in the response.
`,
})
@ApiParam({
@@ -397,6 +770,7 @@ Returns status of each item (uploaded/captured or not).
return await this.claimRequestManagementService.getCaptureRequirementsV2(
claimRequestId,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
@@ -415,7 +789,7 @@ Returns status of each item (uploaded/captured or not).
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
fileSize: DEFAULT_MEDIA_MAX_BYTES,
},
storage: diskStorage({
destination: "./files/claim-documents",
@@ -430,20 +804,19 @@ Returns status of each item (uploaded/captured or not).
)
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Upload Required Document (V2 - Step 4)",
summary: "Upload Required Document (V2 - Step 5)",
description: `
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim)
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
**Upload one of the 13 required documents:**
- car_green_card
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
- damaged_driving_license_front/back
- damaged_chassis_number, damaged_engine_photo
- damaged_car_card_front/back, damaged_metal_plate
- guilty_driving_license_front/back
- guilty_car_card_front/back, guilty_metal_plate
- guilty_driving_license_front/back, guilty_car_card_front/back, guilty_metal_plate (THIRD_PARTY)
**When all 13 documents are uploaded:**
- Workflow automatically moves to: CAPTURE_PART_DAMAGES (Step 5)
**When all required documents are uploaded:** Workflow moves to USER_SUBMISSION_COMPLETE and the claim is ready for damage expert review.
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to upload documents on behalf of the damaged party.
`,
})
@ApiParam({
@@ -459,7 +832,6 @@ Returns status of each item (uploaded/captured or not).
documentKey: {
type: "string",
enum: [
"car_green_card",
"damaged_driving_license_front",
"damaged_driving_license_back",
"damaged_chassis_number",
@@ -473,7 +845,7 @@ Returns status of each item (uploaded/captured or not).
"guilty_car_card_back",
"guilty_metal_plate",
],
example: "car_green_card",
example: "damaged_driving_license_front",
},
file: {
type: "string",
@@ -501,12 +873,14 @@ Returns status of each item (uploaded/captured or not).
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user: any,
): Promise<UploadRequiredDocumentV2ResponseDto> {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
try {
return await this.claimRequestManagementService.uploadRequiredDocumentV2(
claimRequestId,
body,
file,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
@@ -523,7 +897,7 @@ Returns status of each item (uploaded/captured or not).
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
fileSize: DEFAULT_MEDIA_MAX_BYTES,
},
storage: diskStorage({
destination: "./files/claim-captures",
@@ -538,15 +912,17 @@ Returns status of each item (uploaded/captured or not).
)
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Capture Car Angle or Damaged Part (V2 - Step 5)",
summary: "Capture Car Angle or Damaged Part (V2 - Step 4)",
description: `
**Workflow Step:** CAPTURE_PART_DAMAGES (Step 5 of Claim)
**Workflow Step:** CAPTURE_PART_DAMAGES (Step 4 of Claim)
**Capture types:**
1. **angle**: Car angles (front, back, left, right) - 4 required
2. **part**: Damaged parts based on selectedParts from Step 2
**All captures must be completed before user can submit claim.**
**When all captures are complete (parts, angles, capture-phase docs):** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5). Angles are blocked until all parts are captured; capture-phase documents are blocked until all angles are captured.
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party.
`,
})
@ApiParam({
@@ -568,7 +944,7 @@ Returns status of each item (uploaded/captured or not).
type: "string",
example: "front",
description:
'For angle: front/back/left/right. For part: hood/front_bumper/etc.',
"For angle: front/back/left/right. For part: hood/front_bumper/etc.",
},
file: {
type: "string",
@@ -592,12 +968,14 @@ Returns status of each item (uploaded/captured or not).
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user: any,
): Promise<CapturePartV2ResponseDto> {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
try {
return await this.claimRequestManagementService.capturePartV2(
claimRequestId,
body,
file,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
@@ -606,4 +984,136 @@ Returns status of each item (uploaded/captured or not).
);
}
}
/**
* V2: Upload repair factor for a part (same intent as v1 PATCH …/request/reply/:claimRequestId/:partId/upload-factor).
*/
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Upload repair factor file for a factor-needed part (V2)",
description:
"Part must have `factorNeeded: true` on the active reply (`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`). One file per part.\n\n" +
"**Requires:** `ClaimCaseStatus` **`OWNER_REPAIR_FACTOR_UPLOAD_PENDING`** or **`INSURER_REVIEW_MIXED_FACTORS_PENDING`** (or legacy **`WAITING_FOR_INSURER_APPROVAL`**), `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`.\n\n" +
"**Mixed priced+factor replies:** owner must complete **owner-insurer-approval/sign** (priced-line phase) first so `evaluation.ownerPricedPartsApproval` exists.\n\n" +
"When every `factorNeeded` line has `factorLink`, the case moves to **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION` for damage expert validation.",
})
@ApiParam({ name: "claimRequestId", description: "Claim case ID" })
@ApiParam({ name: "partId", description: "Part id from expert reply" })
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
@UseInterceptors(
FileInterceptor("file", {
storage: diskStorage({
destination: "./files/claim-factors",
filename: (req, file, callback) => {
const unique = Date.now();
callback(null, `-${unique}-${file.originalname}`);
},
}),
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
}),
)
async uploadFactorForPartV2(
@Param("claimRequestId") claimRequestId: string,
@Param("partId") partId: string,
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user: any,
) {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
try {
return await this.claimRequestManagementService.uploadClaimFactorV2(
claimRequestId,
partId,
file,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to upload factor",
);
}
}
/**
* V2 API: Upload car walk-around video (same behavior as v1 PATCH claim-request-management/car-capture/:id)
*/
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/car-capture-videos/",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const filename = `claim-video-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiConsumes("multipart/form-data")
@Patch("car-capture/:claimRequestId")
@ApiOperation({
summary: "Upload car walk-around video (V2)",
description:
"Multipart upload of the car capture video during CAPTURE_PART_DAMAGES. " +
"Stores file metadata in `claim-video-capture` and sets `ClaimCase.media.videoCaptureId`. " +
"Only one video per claim; path is stored on the video document (not duplicated on the case).",
})
@ApiParam({
name: "claimRequestId",
description: "The claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiResponse({
status: 200,
description: "Video uploaded successfully",
type: VideoCaptureV2ResponseDto,
})
@ApiResponse({
status: 400,
description: "Wrong workflow step or missing file",
})
@ApiResponse({ status: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Video already uploaded" })
async captureVideoCaptureV2(
@Param("claimRequestId") claimRequestId: string,
@UploadedFile("file") file: Express.Multer.File,
@CurrentUser() user: any,
): Promise<VideoCaptureV2ResponseDto> {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
try {
return await this.claimRequestManagementService.setVideoCaptureV2(
claimRequestId,
file,
user.sub,
user,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to upload car capture video",
);
}
}
}

View File

@@ -1,34 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { CarAngle } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsString } from "class-validator";
/**
* V2 DTO for capturing car angle or damaged part
*/
export class CapturePartV2Dto {
@ApiProperty({
description: 'Type of capture: angle or part',
example: 'angle',
enum: ['angle', 'part'],
description: "Type of capture: angle or part",
example: "angle",
enum: ["angle", "part"],
})
@IsNotEmpty({ message: 'Capture type is required' })
@IsEnum(['angle', 'part'], {
@IsNotEmpty({ message: "Capture type is required" })
@IsEnum(["angle", "part"], {
message: 'Capture type must be either "angle" or "part"',
})
captureType: 'angle' | 'part';
captureType: "angle" | "part";
@ApiProperty({
description: 'Key of the angle or part being captured',
example: 'front',
description:
'When captureType is angle: front | back | left | right. When part: catalog id as string (e.g. "101"), 0-based index (e.g. "0"), or full catalog key (e.g. left_backfender). Prefer id or index for parts.',
example: "front",
})
@IsNotEmpty({ message: 'Capture key is required' })
@IsString({ message: 'Capture key must be a string' })
@IsNotEmpty({ message: "Capture key is required" })
@IsString({ message: "Capture key must be a string" })
captureKey: string;
@ApiProperty({
type: 'string',
format: 'binary',
description: 'Image file (JPG, PNG)',
type: "string",
format: "binary",
description: "Image file (JPG, PNG)",
})
file: Express.Multer.File;
}
@@ -38,44 +38,79 @@ export class CapturePartV2Dto {
*/
export class CapturePartV2ResponseDto {
@ApiProperty({
description: 'Claim request ID',
example: '507f1f77bcf86cd799439011',
description: "Claim request ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: 'Type of capture',
example: 'angle',
description: "Type of capture",
example: "angle",
})
captureType: string;
@ApiProperty({
description: 'Key of what was captured',
example: 'front',
description: "Key of what was captured",
example: "front",
})
captureKey: string;
@ApiProperty({
description: 'File URL',
example: 'http://localhost:3000/files/captures/front-1234567890.jpg',
description: "File URL",
example: "http://localhost:3000/files/captures/front-1234567890.jpg",
})
fileUrl: string;
@ApiProperty({
description: 'Whether all captures are now complete',
description: "Whether all captures are now complete",
example: false,
})
allCapturesComplete: boolean;
@ApiProperty({
description: 'Current workflow step',
example: 'CAPTURE_PART_DAMAGES',
description: "Current workflow step",
example: "CAPTURE_PART_DAMAGES",
})
currentStep: string;
@ApiProperty({
description: 'Success message',
example: 'Angle captured successfully. 6 captures remaining.',
description: "Success message",
example: "Angle captured successfully. 6 captures remaining.",
})
message: string;
@ApiPropertyOptional({
description:
"True when expert-requested part resends are complete and the claim returned to the expert queue.",
})
expertResendComplete?: boolean;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran attachment upload result. Local capture still succeeds when this contains a warning.",
})
fanavaranAttachment?: unknown;
}
/**
* Response DTO for car walk-around video upload (V2)
*/
export class VideoCaptureV2ResponseDto {
@ApiProperty({
description: "Claim case ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: "ID of the stored video document (claim-video-capture)",
example: "507f1f77bcf86cd799439012",
})
videoId: string;
@ApiProperty({
description: "Success message",
example: "Video capture uploaded successfully.",
})
message: string;
}

View File

@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional } from 'class-validator';
/**
* DTO for required document item
@@ -34,6 +35,13 @@ export class RequiredDocumentItem {
enum: ['general', 'damaged_party', 'guilty_party'],
})
category: string;
@ApiPropertyOptional({
description:
'When true, the client should upload this file during CAPTURE_PART_DAMAGES (same POST upload-document endpoint and `requiredDocuments` keys). Capture cannot finish until these are uploaded.',
example: true,
})
preferUploadDuringCapture?: boolean;
}
/**
@@ -71,10 +79,17 @@ export class CarAngleItem {
*/
export class DamagedPartItem {
@ApiProperty({
description: 'Part key',
example: 'hood',
description:
'Side-agnostic part name (matches catalog suffix); use with `side` to disambiguate',
example: 'backfender',
})
key: string;
name: string;
@ApiPropertyOptional({
description: 'Vehicle side / region (left, right, front, back, top)',
example: 'left',
})
side?: string;
@ApiProperty({
description: 'Display label in Farsi',
@@ -82,6 +97,12 @@ export class DamagedPartItem {
})
label_fa: string;
@ApiPropertyOptional({
description: 'Deprecated: same as `name` (kept for older clients)',
example: 'backfender',
})
key?: string;
@ApiProperty({
description: 'Display label in English',
example: 'Hood',
@@ -93,6 +114,12 @@ export class DamagedPartItem {
example: false,
})
captured: boolean;
/** Static catalog id (same as `damagedParts[].id` in capture requirements) when the part comes from the outer-parts catalog. */
@ApiPropertyOptional({ example: 12 })
@IsOptional()
@IsInt()
id?: number;
}
/**
@@ -117,6 +144,20 @@ export class GetCaptureRequirementsV2ResponseDto {
})
currentStep: string;
@ApiProperty({
description:
'Ordered capture phase during CAPTURE_PART_DAMAGES: parts → angles → capture_phase_documents',
example: 'angles',
enum: ['parts', 'angles', 'capture_phase_documents', 'complete'],
})
captureSequencePhase: string;
@ApiProperty({
description: 'Human-readable hint for what the user should do next in the capture step',
example: 'Capture all four car angles (front, back, left, right) next.',
})
captureSequenceHint: string;
@ApiProperty({
description: 'List of required documents to upload',
type: [RequiredDocumentItem],
@@ -159,5 +200,7 @@ export class GetCaptureRequirementsV2ResponseDto {
anglesTotal: number;
partsCaptured: number;
partsTotal: number;
capturePhaseDocsRemaining: number;
postCaptureDocumentsRemaining: number;
};
}

View File

@@ -1,4 +1,94 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { DamageSelectedPartV2BodyDto } from './damage-selected-part-v2.dto';
/** Suggested HTTP call for the owner UI (`pathTemplate`: replace placeholders). */
export class ClaimDetailsOwnerNextActionV2Dto {
@ApiProperty({ description: 'Stable UI key', example: 'FINAL_SIGN' })
key: string;
@ApiPropertyOptional({ description: 'HTTP verb', example: 'PUT' })
method?: string;
@ApiPropertyOptional({
description: 'Path under API root',
example: 'v2/claim-request-management/request/{claimRequestId}/owner-insurer-approval/sign',
})
pathTemplate?: string;
@ApiProperty({ description: 'What this endpoint does for the user' })
description: string;
}
/** Server-derived hints: which phase the claim is in and what to offer next (owners only). */
export class ClaimDetailsOwnerGuidanceV2Dto {
@ApiProperty({
description: 'Machine-readable phase',
enum: [
'COMPLETED',
'REJECTED',
'CANCELLED',
'EXPERT_RESEND',
'WAITING_DAMAGE_EXPERT',
'EXPERT_REVIEWING',
'USER_FLOW',
'UPLOAD_FACTORS',
'EXPERT_VALIDATING_FACTORS',
'SIGN_PRICED_LINES',
'INSURER_REVIEW_NEEDS_REVISION',
'FINAL_SIGN_OR_REJECT',
'INSURER_APPROVAL_FALLBACK',
],
})
phaseKey: string;
@ApiProperty({ description: 'Short headline for the current phase' })
headline: string;
@ApiPropertyOptional({ description: 'Longer UX copy' })
detail?: string;
@ApiProperty({ type: [ClaimDetailsOwnerNextActionV2Dto] })
nextActions: ClaimDetailsOwnerNextActionV2Dto[];
@ApiPropertyOptional({
description: 'Whether PUT objection is permitted (see server validation for exact rules)',
})
objectionAllowed?: boolean;
@ApiPropertyOptional({ description: 'How objection relates to priced vs factor-only lines' })
objectionHint?: string;
}
/** Active damage-expert resend request (owner must upload or acknowledge). */
export class ExpertResendDetailsV2Dto {
@ApiPropertyOptional()
resendDescription?: string;
@ApiPropertyOptional({ type: [String] })
resendDocuments?: string[];
@ApiPropertyOptional({
description:
"Damaged parts the expert asked to re-capture (same shape as damagedParts: id, name, side, label_fa, catalogKey, captured, url).",
type: [Object],
})
resendCarParts?: Array<{
id?: number | null;
name?: string;
side?: string;
label_fa?: string;
label_en?: string;
catalogKey?: string;
key?: string;
captured?: boolean;
url?: string;
path?: string;
fileName?: string;
}>;
@ApiPropertyOptional({ description: 'Set when the owner satisfied the resend request' })
fulfilledAt?: Date;
}
export class ClaimDetailsV2ResponseDto {
@ApiProperty({ description: 'Claim case ID' })
@@ -10,7 +100,11 @@ export class ClaimDetailsV2ResponseDto {
@ApiProperty({ description: 'Request number' })
requestNo: string;
@ApiProperty({ description: 'Overall case status' })
@ApiProperty({
description:
"ClaimCaseStatus; see also `ownerGuidance` for UX. New V2–V5 priced-only claims are COMPLETED after expert work; factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING | OWNER_REPAIR_FACTOR_UPLOAD_PENDING | EXPERT_VALIDATING_REPAIR_FACTORS. V5 then uses WAITING_FOR_FILE_MAKER_APPROVAL. Legacy WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN may still appear.",
example: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
})
status: string;
@ApiProperty({ description: 'Claim damage status' })
@@ -28,9 +122,25 @@ export class ClaimDetailsV2ResponseDto {
@ApiPropertyOptional({ description: 'Blame request number' })
blameRequestNo?: string;
@ApiPropertyOptional({ description: 'Owner info' })
@ApiPropertyOptional({
description: 'Blame file type: THIRD_PARTY or CAR_BODY',
example: 'THIRD_PARTY',
})
blameType?: string;
@ApiPropertyOptional({
description: 'How the blame file was initiated: IN_PERSON or LINK',
example: 'IN_PERSON',
})
creationMethod?: string;
@ApiPropertyOptional({
description: 'Claim owner (damaged party): ids for the user and their insurer client scope',
})
owner?: {
userId: string;
clientId?: string;
userClientKey?: string;
fullName?: string;
};
@@ -42,8 +152,11 @@ export class ClaimDetailsV2ResponseDto {
plate?: any;
};
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
selectedParts?: string[];
@ApiPropertyOptional({
description: 'Selected outer damaged parts (ordered objects with id, name, side, label_fa)',
type: [DamageSelectedPartV2BodyDto],
})
selectedParts?: DamageSelectedPartV2BodyDto[];
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
otherParts?: string[];
@@ -54,14 +167,83 @@ export class ClaimDetailsV2ResponseDto {
nationalCodeOfOwner?: string;
};
@ApiPropertyOptional({ description: 'Required documents status' })
requiredDocuments?: Record<string, { uploaded: boolean; fileId?: string }>;
@ApiPropertyOptional({ description: 'Required documents status (link instead of id)' })
requiredDocuments?: Record<string, { uploaded: boolean; fileUrl?: string }>;
@ApiPropertyOptional({ description: 'Car angles captured' })
carAngles?: Record<string, { captured: boolean; url?: string }>;
@ApiPropertyOptional({ description: 'Damaged parts captured' })
damagedParts?: Record<string, { captured: boolean; url?: string }>;
@ApiPropertyOptional({
description:
'Per-part capture status and URLs (array index aligns with selectedParts; includes id, name, side, label_fa)',
type: 'array',
items: {
type: 'object',
properties: {
index: { type: 'number' },
id: { type: 'number', nullable: true },
name: { type: 'string' },
side: { type: 'string' },
label_fa: { type: 'string' },
captured: { type: 'boolean' },
url: { type: 'string' },
path: { type: 'string' },
fileName: { type: 'string' },
},
},
})
damagedParts?: Array<{
index: number;
id?: number | null;
name: string;
side: string;
label_fa: string;
captured: boolean;
url?: string;
path?: string;
fileName?: string;
}>;
@ApiPropertyOptional({
description:
'Damage expert resend instructions and progress (when status is WAITING_FOR_USER_RESEND).',
})
expertResend?: ExpertResendDetailsV2Dto;
@ApiPropertyOptional({
description:
'Fanavaran claim reference. Returned only after the local claim reaches COMPLETED and Fanavaran has supplied at least one reference.',
example: { claimNo: 123456, claimId: 987654 },
})
fanavaran?: {
claimNo?: number;
claimId?: number;
};
@ApiPropertyOptional({
description: "Damage expert opinion(s): initial and final (after objection).",
type: Object,
})
evaluation?: {
damageExpertReply?: unknown;
damageExpertReplyFinal?: unknown;
};
@ApiPropertyOptional({
type: ClaimDetailsOwnerGuidanceV2Dto,
description:
'Owner-only: derived headline, suggested API routes, and whether objection is plausible. Omitted when the actor is FIELD_EXPERT.',
})
ownerGuidance?: ClaimDetailsOwnerGuidanceV2Dto;
@ApiPropertyOptional({ description: 'User satisfaction rating (if submitted)' })
userRating?: {
progressSpeed: number;
registrationEase: number;
overallEvaluation: number;
comment?: string;
createdAt?: Date;
};
@ApiProperty({ description: 'Created at' })
createdAt: string;

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export class CreateClaimFromBlameResponseDto {
@ApiProperty({
@@ -23,4 +23,10 @@ export class CreateClaimFromBlameResponseDto {
example: "Claim request created successfully",
})
message: string;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran early submit result. Claim creation still succeeds when this contains a warning.",
})
fanavaran?: unknown;
}

View File

@@ -0,0 +1,27 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsInt, IsOptional, IsString } from "class-validator";
/** Stored shape / API payload for one selected outer damaged part (V2). */
export class DamageSelectedPartV2BodyDto {
@ApiPropertyOptional({ description: "Catalog id when from outer-parts catalog" })
@IsOptional()
@IsInt()
id?: number;
@ApiProperty({ example: "backfender" })
@IsString()
name: string;
@ApiProperty({ example: "left" })
@IsString()
side: string;
@ApiProperty({ example: "گلگیر عقب" })
@IsString()
label_fa: string;
@ApiPropertyOptional({ description: "Original catalog key, e.g. left_backfender" })
@IsOptional()
@IsString()
catalogKey?: string;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Claim case ID', example: '507f1f77bcf86cd799439011' })
@@ -10,7 +10,11 @@ export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Claim request number', example: 'CL12345' })
requestNo: string;
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' })
@ApiProperty({
description:
"ClaimCaseStatus. New V2–V5 priced-only claims become COMPLETED after expert work. Factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING (priced-line acceptance before factor uploads), OWNER_REPAIR_FACTOR_UPLOAD_PENDING, and EXPERT_VALIDATING_REPAIR_FACTORS. V5 then waits at WAITING_FOR_FILE_MAKER_APPROVAL. Legacy DB rows may still use WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN.",
example: "COMPLETED",
})
status: string;
@ApiProperty({ description: 'Claim damage determination status', example: 'PENDING' })
@@ -24,12 +28,35 @@ export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Blame request ID this claim originated from' })
blameRequestId?: string;
@ApiPropertyOptional({
description: 'Blame file type: THIRD_PARTY or CAR_BODY',
example: 'THIRD_PARTY',
})
blameType?: string;
@ApiPropertyOptional({
description: 'How the blame file was initiated: IN_PERSON or LINK',
example: 'IN_PERSON',
})
creationMethod?: string;
}
export class GetMyClaimsV2ResponseDto {
@ApiProperty({ description: 'List of user claims', type: [ClaimListItemV2Dto] })
list: ClaimListItemV2Dto[];
@ApiProperty({ description: 'Total count', example: 5 })
@ApiProperty({ description: 'Total count after search filter', example: 5 })
total: number;
@ApiPropertyOptional({
description: 'Current page when `page` or `limit` query params were sent',
})
page?: number;
@ApiPropertyOptional({ description: 'Page size when paginating' })
limit?: number;
@ApiPropertyOptional({ description: 'Total pages when paginating' })
totalPages?: number;
}

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