forked from Yara724/api
Compare commits
779 Commits
integrate-
...
c72cb265f1
| Author | SHA1 | Date | |
|---|---|---|---|
| c72cb265f1 | |||
| 271dc4df2a | |||
| b2ff3f574b | |||
| 769a581a51 | |||
| 466773fb2b | |||
| 37041ba19f | |||
| 0b0a1dfa13 | |||
| 073b4dec38 | |||
|
|
cb4f02918f | ||
| c1f0d041ce | |||
| f9c60a2854 | |||
| 4310ce6398 | |||
|
|
96b56233bf | ||
| d6b5f8f4d4 | |||
|
|
152250a387 | ||
| ada85f9e0c | |||
|
|
e7103a9408 | ||
| af8bec193b | |||
|
|
9930e9ff5b | ||
| b3a298e254 | |||
| b13b0211ad | |||
| 6e5b7cb1fd | |||
|
|
51166a8d0d | ||
| 4f301d1d03 | |||
|
|
1b69da38ff | ||
| 1941013b00 | |||
|
|
98c7ebb83a | ||
| 461afbb6b9 | |||
|
|
6a26811555 | ||
|
|
f9d4b5df9a | ||
|
|
0eaf55e73c | ||
|
|
5b77241f28 | ||
|
|
cc8a96c875 | ||
|
|
5812637a1e | ||
|
|
287978a555 | ||
|
|
fe22e69351 | ||
|
|
b5b18114d8 | ||
|
|
d060b6d9e2 | ||
|
|
c64f23091a | ||
|
|
401ad6a143 | ||
| ed9ef3bac3 | |||
|
|
bc9c217d8a | ||
| 48c8290f9c | |||
|
|
7fd60df1d6 | ||
| 7f50b75c64 | |||
|
|
9e51fdcdaf | ||
| 58e374d502 | |||
|
|
0bab93e983 | ||
|
|
fc37dd78a4 | ||
| 795ecbae96 | |||
|
|
15342e16d8 | ||
| 2dc25100ea | |||
|
|
ebe45646c1 | ||
| bf515f3011 | |||
|
|
55da8188a9 | ||
| 20aa6957e9 | |||
|
|
0e861ff6d1 | ||
| eb6cef1127 | |||
| f98712ba83 | |||
| 3635dc2c10 | |||
|
|
f037450958 | ||
| 503894bf9c | |||
| b9bffdccc1 | |||
| 395e3dbe63 | |||
|
|
9fc7198f40 | ||
| d860e94dee | |||
| b7857a40f5 | |||
| c7cd9e8ce5 | |||
| 6100ac80f3 | |||
|
|
229957e283 | ||
| 0724d66727 | |||
|
|
7e3b308572 | ||
| 4423b7fa25 | |||
|
|
519967855e | ||
| 01346f950c | |||
| 80e112f09b | |||
| 80b71107a1 | |||
| 470f6e0920 | |||
| 584a550ce2 | |||
|
|
8b34e94de3 | ||
| 706bffd1e7 | |||
|
|
ef9768795d | ||
| 4d2501d90c | |||
|
|
c9b5b6f765 | ||
| d52d18a97c | |||
| 02edd0b2ca | |||
| 5f714e1456 | |||
|
|
233a16d96c | ||
| 6ecbe91ede | |||
| eb636bdbdd | |||
| eb072ef080 | |||
|
|
e48dffef89 | ||
| 4f7ee9a0de | |||
|
|
feacad58ca | ||
| 70fa49398d | |||
| 5c2b660600 | |||
| ebfeef9e01 | |||
| cf74d75146 | |||
| f5a60eafb1 | |||
|
|
ae49031c55 | ||
| 23d636416d | |||
|
|
6880de5960 | ||
| 85bd892720 | |||
|
|
a33466025d | ||
| a5d2f5a2b9 | |||
|
|
226b63aefb | ||
| 5203c07154 | |||
|
|
6566b5f112 | ||
| 24a38a6780 | |||
|
|
dc3748ae94 | ||
| fe83c20e7e | |||
|
|
e5a28238e5 | ||
| 8cf7b7b229 | |||
|
|
e4ef95a23e | ||
| 5ec40f66dc | |||
|
|
f6f4429ce7 | ||
|
|
2df7a889d3 | ||
| 93edba412f | |||
|
|
3bd6bc6e9d | ||
|
|
26c80512ba | ||
| 85fc83b564 | |||
|
|
f09f5b79f0 | ||
| c7d77d30c4 | |||
| 693d602e49 | |||
| cb69b496b5 | |||
| 5942f51b4c | |||
|
|
bfbcfcab37 | ||
| 0f9f702a00 | |||
| 1d56326456 | |||
|
|
d5aa9f3f1b | ||
| d8a2be091f | |||
|
|
01f8a5b12c | ||
| e6ea5c2e8a | |||
|
|
875b52d761 | ||
| f5aa25edf8 | |||
|
|
85d7881b2b | ||
| 7259eec949 | |||
|
|
42f4c6e8e3 | ||
| 71b3b1d786 | |||
|
|
baac633443 | ||
| 13231736d8 | |||
|
|
cbed681c8f | ||
| 3e1cde739f | |||
|
|
70d7f34402 | ||
| f57b1b1171 | |||
|
|
6791c71809 | ||
| 6d955cd608 | |||
|
|
ca7200e17d | ||
| 60ed80fc87 | |||
|
|
210e96fcf1 | ||
| 63038f630d | |||
|
|
8e4c794d61 | ||
| 0663b35157 | |||
|
|
d4cd8c9343 | ||
| 2e8a8197a4 | |||
|
|
3821bf36ef | ||
| 7f131eb83e | |||
| 03ebc6649c | |||
| 448e5fb9ba | |||
| f53ca43e54 | |||
|
|
473075e546 | ||
| 26da533a52 | |||
| a03d557718 | |||
| 5a4a511e84 | |||
| 91028f999c | |||
| 07d6cdbe7c | |||
| e6038c73c5 | |||
| d4487776fb | |||
| 07cd4776e9 | |||
|
|
ab1e64c87b | ||
| dd6ee8ee20 | |||
|
|
cb23455bcd | ||
| d528af5c1d | |||
|
|
793dc52640 | ||
| 01c1be40c7 | |||
| 738344a9d4 | |||
| 767317cce8 | |||
|
|
65870c8d66 | ||
| 70d05a7624 | |||
|
|
192d4e72de | ||
| 305a2965bf | |||
|
|
626b0ded34 | ||
| c4f3558cda | |||
|
|
c8ccd943f2 | ||
| e761c4b6b2 | |||
| 7f672541ae | |||
| ab4f667c8d | |||
|
|
0b9bd59a66 | ||
| fdfca80eb6 | |||
| f7b7cd13e8 | |||
| ca5c1900ff | |||
| fc7488a204 | |||
| 298761233a | |||
| 05531260da | |||
| b345818d43 | |||
| c2f5c576fa | |||
| 8f66502c49 | |||
| 6a26eaa6da | |||
| 86f8b829fd | |||
| ea3db8f025 | |||
| 2cada1eba0 | |||
| 8448d2d771 | |||
| 77cf420c33 | |||
| bf7e2ef97a | |||
| 426268fed2 | |||
| a0874f4837 | |||
| 0c0c306740 | |||
| 914e97687f | |||
| 6b34b0cdf7 | |||
|
|
e742d43201 | ||
| 5e3da9dc02 | |||
|
|
ea4fcbe713 | ||
| 5eb50f94cc | |||
|
|
b0c7a0890c | ||
|
|
637cbb20d5 | ||
| d72c811659 | |||
|
|
45630a33ef | ||
| 50ea476fa5 | |||
| 8609dbc377 | |||
| 0019a22ccd | |||
| 8aae4b75e1 | |||
|
|
891b1b2bbc | ||
| 7ed102c6ef | |||
|
|
ec0d03fadf | ||
| 75adb2a3d2 | |||
|
|
4cf0461a48 | ||
| d7e6784746 | |||
| d119cd64aa | |||
|
|
021d749962 | ||
|
|
61684156c6 | ||
| 639e6ceebb | |||
|
|
588a92c4b4 | ||
| f2e20b32eb | |||
|
|
c94dd27a96 | ||
| 6e1405c309 | |||
| f053ee1348 | |||
| 182db56e15 | |||
| 9b348d567d | |||
|
|
e4c3b7a16a | ||
| 23fca04705 | |||
|
|
4b9d946bfd | ||
| c85503e598 | |||
|
|
9828aee8af | ||
| 778544c321 | |||
|
|
3afff67336 | ||
| 793ff639ba | |||
|
|
ec15cff557 | ||
| fd42adf9d6 | |||
|
|
4272790fad | ||
| ac65cdb77b | |||
| 8430c68e3a | |||
| 49fe548215 | |||
|
|
777eae1028 | ||
| b67dd733cd | |||
|
|
4818f73252 | ||
| cc8a5354c7 | |||
|
|
2d5ade33d2 | ||
| b73c92c21f | |||
| f9f462d47b | |||
| c3eb36dc41 | |||
|
|
f75ecf5c2c | ||
| 75c4cb6a05 | |||
|
|
7a4277f8b2 | ||
| e50bc78344 | |||
|
|
2c1cd93dd0 | ||
| ffd44df718 | |||
|
|
64865b70f2 | ||
| c207c30be9 | |||
|
|
fcb169e9ac | ||
| 1867292499 | |||
|
|
2cc96f6132 | ||
| 4d5b91d4fe | |||
| 8ba97537a4 | |||
| 70160543a2 | |||
|
|
8460c86820 | ||
| 61f4181065 | |||
|
|
4058cb4a61 | ||
| b2ad43d050 | |||
|
|
9fc4ad9931 | ||
| 213cdd765b | |||
|
|
06d69aa4d0 | ||
| 318a5c74dd | |||
|
|
7241ae3270 | ||
| 1f4a520145 | |||
|
|
59a1b9064e | ||
| 0420eda35f | |||
|
|
482d2b01f1 | ||
|
|
04d7966776 | ||
| b129c1ef9b | |||
|
|
a1fca82cb2 | ||
|
|
5b114c2069 | ||
| 5e4897f609 | |||
| a79c3ca05f | |||
| 36fa1c552e | |||
| 9742fecc11 | |||
| 8007c30efd | |||
| 144f8f3e2a | |||
| 8674dd8762 | |||
|
|
f39c50aeb0 | ||
| 2fda740171 | |||
| 72e5bd616c | |||
| 4b41a60f64 | |||
| 9310285bd4 | |||
|
|
2a8b66bc16 | ||
| 9168a6bdcd | |||
|
|
057bedeb0c | ||
| 808a3b8526 | |||
|
|
da7a4f8890 | ||
| 21e55012be | |||
|
|
385757c3a0 | ||
| a1b122a33b | |||
|
|
aec9e76918 | ||
| 7c59c2407e | |||
|
|
168e52a475 | ||
|
|
4aa6e03afb | ||
|
|
36a34e27b3 | ||
|
|
6387ebaed0 | ||
| 22a5990934 | |||
|
|
e5de99adde | ||
| c7fd2a6b33 | |||
|
|
5595083e86 | ||
| 2296fa5d86 | |||
|
|
72dec7a917 | ||
| 67019851de | |||
|
|
c955deda5c | ||
|
|
9b83db882b | ||
|
|
bced6a0ec7 | ||
| 5d1110b6e9 | |||
|
|
a7fe04c032 | ||
|
|
0dcb2cf2ca | ||
| 8b125af4e7 | |||
| 1559a40213 | |||
|
|
54ae82aa38 | ||
|
|
7a3ddcc7be | ||
|
|
da3f57870e | ||
| ac7ee941b8 | |||
| 5d005d5eee | |||
| b65d9bfe81 | |||
|
|
04f51167c2 | ||
|
|
2c8fd3960f | ||
|
|
e59058520c | ||
| 80122e7772 | |||
| f409d78ede | |||
|
|
24340eb810 | ||
| 41d1de77eb | |||
| 0aadc64cd3 | |||
|
|
1e6c36cbd4 | ||
| ae23a10d33 | |||
| 1c6678d8e4 | |||
|
|
ad5ff28be4 | ||
| 1fe2c77c70 | |||
| 45a51f2c24 | |||
|
|
0514548136 | ||
| 5b4cc0560f | |||
| e9c02811f7 | |||
| 8ab49c9a35 | |||
| f0cd4461a8 | |||
| 3205a89611 | |||
| 14bc075521 | |||
| 1fe66b2d91 | |||
| 033a853b51 | |||
| f05891a112 | |||
| 7641c56440 | |||
| ae83100ef4 | |||
| 9e2cf9bcdf | |||
| ced08fc1f7 | |||
| d525b8dd0d | |||
| b43f1a86dc | |||
|
|
5df39b502e | ||
| 49564cc1c6 | |||
|
|
29939eee20 | ||
| ae789323d8 | |||
|
|
6be9ff16e1 | ||
|
|
0c5a2fe38b | ||
| 5ef8310cb0 | |||
| d2cb9444f3 | |||
| 67471fb9ce | |||
| 569e7592ec | |||
|
|
e2a9232523 | ||
|
|
dc006735ba | ||
|
|
f92cee0575 | ||
|
|
493be68b80 | ||
|
|
edf027acd3 | ||
| 0698338d4c | |||
|
|
f3c7f6a7e0 | ||
| 607472cd89 | |||
|
|
65e7476642 | ||
| 9068765c25 | |||
| 99c819caeb | |||
| 8d396762a2 | |||
| f3686575ca | |||
|
|
b9fe1bfd52 | ||
| 6eca1f5dad | |||
|
|
6477778835 | ||
|
|
4552450fbc | ||
|
|
f7f7f4548d | ||
|
|
220b39ea5a | ||
| 4a045f564c | |||
|
|
d3249e9854 | ||
| 7e597db423 | |||
| 8303bf4467 | |||
| ebe8671bd3 | |||
| 5022178569 | |||
| 2fbf40ef19 | |||
| dca9e1f8d4 | |||
|
|
8f8ed8a94e | ||
|
|
3c7a656cd7 | ||
| 87ece0d89d | |||
| 7af3f3627a | |||
| 7e1ae328ac | |||
| 094d9048ba | |||
| 9afb6a8a6e | |||
| 2df25e26bb | |||
| c7fb6174a0 | |||
| 136b22fd81 | |||
| 16cdf2e7b0 | |||
| 488c9180af | |||
| 75c556a013 | |||
| 0f53bfabf5 | |||
| 8bf3cfe5e9 | |||
|
|
523172da67 | ||
|
|
2fc6015213 | ||
| f6ec7644ff | |||
| bc70a8c930 | |||
|
|
cca3ed01a4 | ||
| 4caa958175 | |||
| 7d10c00ce5 | |||
| 114e5e6604 | |||
| f00cb226a7 | |||
|
|
e2b879d943 | ||
| d84bd24682 | |||
|
|
ed2b6948cf | ||
|
|
c6b417ced7 | ||
| 16d3c54613 | |||
|
|
db569db9d1 | ||
| 83e82ea68e | |||
|
|
8f29bb564c | ||
| 89e715b0c9 | |||
| 44f7ce5b54 | |||
| a2396da9e4 | |||
| 64f6245e06 | |||
| df79a4d307 | |||
| 65c30a6cba | |||
| 0dd6c8ff78 | |||
| 0442d04f20 | |||
|
|
d0e7694374 | ||
| 570fa865de | |||
|
|
8741d2ba82 | ||
| 7b53c98791 | |||
|
|
59eddb8e0e | ||
| 4e20fc5c96 | |||
|
|
4fabed77e5 | ||
|
|
2e4b10455b | ||
| 1bbf0de960 | |||
|
|
15fcb011aa | ||
| 2c52c14e03 | |||
|
|
5a89a0ff16 | ||
| d355771518 | |||
| f4301428c7 | |||
| e3406f7645 | |||
| 4d6183fa24 | |||
| ce4945ebb8 | |||
| 5cada3c6c8 | |||
| 761f0cb679 | |||
| 3c61e4397a | |||
| fae7e9b13b | |||
| 3c863bb90c | |||
| 0c5756d325 | |||
| 1670c9d145 | |||
| 92e05d2a49 | |||
| dcc3ee71de | |||
| fa188862e5 | |||
| d8f7766f10 | |||
| 084d0e1360 | |||
| 0111f3acd2 | |||
| 1ef17ce337 | |||
|
|
6df8044c5a | ||
|
|
bc5be99b59 | ||
|
|
a4eb98258b | ||
|
|
ad35d35065 | ||
|
|
4bd88ff0dd | ||
|
|
b008eda11b | ||
| 1680fdc5b4 | |||
|
|
921f9719c7 | ||
|
|
a7849f915a | ||
|
|
19dc2a76f2 | ||
|
|
41f81a2f76 | ||
| 048398d653 | |||
|
|
79905345e5 | ||
|
|
0ed7cd7012 | ||
| 1e7b0d7d06 | |||
|
|
6426233350 | ||
| 3e5e9852ad | |||
|
|
3d6b9e130c | ||
|
|
ec07d42ced | ||
| 407f58f5ee | |||
|
|
c8274d8435 | ||
| f0b24dcd26 | |||
|
|
5414d9717e | ||
|
|
e413991e7c | ||
| b144458943 | |||
|
|
3abbd45fac | ||
| cb47069e90 | |||
| 3c3b5191fb | |||
| 8053e1a088 | |||
|
|
d276c32e87 | ||
| 951ff9b50b | |||
|
|
a59e4c57a5 | ||
| 33c9811f61 | |||
|
|
cab584410f | ||
| c413bd3417 | |||
|
|
249c359898 | ||
| 393d43c4d2 | |||
|
|
34142942e5 | ||
|
|
f023d0f3e7 | ||
| 2d7afba75d | |||
|
|
9ee933cb76 | ||
|
|
16c598118d | ||
| 2b1edd64c1 | |||
|
|
d92231e517 | ||
| dc14698823 | |||
|
|
8236f0440d | ||
| ffcedcd5f1 | |||
|
|
bd5a33e2ba | ||
|
|
0b47e8789b | ||
|
|
2c810afcb6 | ||
|
|
077bae429e | ||
| 456135ad08 | |||
| eae46c3212 | |||
| fe82455562 | |||
| f2d7e39487 | |||
| 8730e9af62 | |||
| cf07122710 | |||
|
|
a0247d7769 | ||
| fcf3e63f9b | |||
| 519d91102f | |||
| 4bc5889ccd | |||
| a47c6f1c96 | |||
|
|
5fab0a00b6 | ||
| e650abdf40 | |||
|
|
6261af8a29 | ||
|
|
fde6464739 | ||
| a07b5c3c1e | |||
|
|
06af79fa47 | ||
| 1a8181a872 | |||
|
|
4a189ba4ef | ||
| 859244940c | |||
|
|
cec349b7c2 | ||
| 8d8f76eadd | |||
|
|
b9e7373225 | ||
| 6ddb06594b | |||
|
|
e90c6a5c50 | ||
| 97f26d400f | |||
|
|
02a69f3db2 | ||
| c137d6c6c4 | |||
|
|
2b7192151d | ||
| 389133e1c9 | |||
|
|
768d6d12fe | ||
|
|
84b752c6cc | ||
| 0622ceeaf4 | |||
|
|
3b0db0d250 | ||
| c502adbe76 | |||
|
|
7aada14551 | ||
| b3bf1b85f8 | |||
|
|
d6f1cb9eeb | ||
|
|
da298a3350 | ||
| 722cbbf5c9 | |||
|
|
245160bfc2 | ||
| 9c760d59f7 | |||
|
|
6ac0bf060e | ||
| 10df869efb | |||
| 5c372947dd | |||
|
|
9003a7abb6 | ||
| a994331439 | |||
|
|
ae12049e1b | ||
| 02031efdb5 | |||
|
|
48cc4d8a8d | ||
| cbbb45378d | |||
|
|
680f3c1798 | ||
|
|
64fa560f73 | ||
|
|
ff94fa35bf | ||
| 037d9fa934 | |||
|
|
2bdd0d507e | ||
| f60efa52b1 | |||
|
|
866696094f | ||
| ed936ad7b1 | |||
|
|
4d4106a8ab | ||
|
|
af875a4773 | ||
| 91221a6848 | |||
|
|
a31196774c | ||
| d2474d65bc | |||
|
|
94bde88cb6 | ||
| 39c4855b95 | |||
|
|
35487ad033 | ||
| 3dec22595c | |||
|
|
44723259d6 | ||
| c96e361990 | |||
|
|
e4dfe7c572 | ||
| 511d478064 | |||
|
|
cef684e37f | ||
| c81157c431 | |||
|
|
e1954cdb37 | ||
|
|
7ff3e9fd10 | ||
| ced8586710 | |||
|
|
f0ba8949cb | ||
| fb224360ab | |||
|
|
600c6bd7ed | ||
| 129be58cc9 | |||
|
|
094816ce8f | ||
| f2848a6179 | |||
|
|
7797a4ddab | ||
| 09eb6cc5c0 | |||
|
|
7c76149c95 | ||
| d562e09aab | |||
|
|
2c851725a5 | ||
| e4d6246103 | |||
|
|
b9d15d1ff6 | ||
| 241634b149 | |||
|
|
7ba3b57cee | ||
|
|
5b6409fc2e | ||
| fd4cd3128f | |||
| 0d0cec4b20 | |||
|
|
e26c533a52 | ||
|
|
f75e6a4453 | ||
|
|
e89cf107ff | ||
| 07c4e5126a | |||
|
|
010846acd9 | ||
|
|
cc926d4668 | ||
| fdb75d52f7 | |||
|
|
186f6c5837 | ||
|
|
3fb90cf1c9 | ||
| 5e5ad0e1b3 | |||
|
|
82bb232d28 | ||
| 45392ad268 | |||
|
|
9c62dc4d3a | ||
| d1bc64bb6d | |||
|
|
be58b7e47e | ||
| 78e86e5745 | |||
|
|
fe163419c0 | ||
|
|
eb648d8a87 | ||
| b856cb59f9 | |||
|
|
a52b7a0a72 | ||
|
|
7998649a89 | ||
|
|
74c91c73b6 | ||
|
|
9e2cec5bc3 | ||
| e95cb4c255 | |||
|
|
c1a54baaf0 | ||
| f8193b6622 | |||
| 3e889307a1 | |||
|
|
972b4b8719 | ||
| 96c21294db | |||
| c579f8fa1d | |||
| c2d59112cf | |||
|
|
e1115b0632 | ||
| 908292b0c3 | |||
| 55ec6f1fd1 | |||
| d33cff8438 | |||
| 0bb13f4596 | |||
| 70306d42e0 | |||
| d9dc4ecdff | |||
| f66fa5d7b4 | |||
| 6429cb0f2b | |||
| 6f120b0066 | |||
| 8419ec06ae | |||
| def4023185 | |||
| 40606fecf1 | |||
| c567f93e85 | |||
| a9846095c1 | |||
| b6f0e3c821 | |||
| bffb8a3b97 | |||
| 715a9f2467 | |||
| 3813c2b3e3 | |||
| ebf8a9a624 | |||
| 993d809de2 | |||
| 80ef885d3b | |||
|
|
3f608f63f1 | ||
| 6019c9e954 | |||
|
|
8ae6f2c91b | ||
|
|
f999313476 | ||
|
|
98f1d2caf5 | ||
|
|
bbd83da2d5 | ||
|
|
9296795166 | ||
|
|
f456443342 | ||
|
|
bcedd8c6f3 | ||
| 8caf13cf18 | |||
|
|
e90f8dc33c | ||
| 4c4b1a1db7 | |||
|
|
c2f996cc28 | ||
| 362e02ddc4 | |||
|
|
4b0e8a3547 | ||
|
|
885678df7d | ||
| 8dffc46357 | |||
|
|
b5b3b722c6 | ||
|
|
4f8cb43883 | ||
|
|
6c2d178686 | ||
| 9854a58282 | |||
|
|
7184142137 | ||
| d45975b6a7 | |||
|
|
05c5b70b4d | ||
|
|
5d2227b00b | ||
| eef305c42e | |||
|
|
9c87927e6c | ||
| 5618d120e3 | |||
|
|
d9b1537ee4 | ||
| f01882dc3a | |||
|
|
8284aba825 | ||
| 3c15901ecc | |||
|
|
1e1edf9136 | ||
| daad9539ea | |||
|
|
194b71cdb2 | ||
|
|
04ba46ed2a | ||
| c6f7edabd2 | |||
| 75b7e5ecad | |||
| a3156881b8 | |||
| 41c44dcf23 | |||
|
|
95bfc095ce | ||
| b63c2155bc | |||
| 9463142ecf | |||
| a71c4ea980 | |||
|
|
b826a133a4 | ||
| b4501c503e | |||
|
|
a0d337aff0 | ||
| 28e2de459a | |||
| af1a07743e | |||
| fca88bc151 | |||
|
|
e264695db3 | ||
| 277f2d4d66 | |||
|
|
514018beb1 | ||
| 374b15933d | |||
| 08a4d754c1 | |||
| c5b2d7b520 | |||
|
|
5a84080bf1 | ||
| 68b3b2000b | |||
|
|
028ca899fe | ||
|
|
0086b8db4d | ||
|
|
7f5b64f2a6 | ||
| 2898897dd9 | |||
| e8a3873851 | |||
| 131df63a3f | |||
| b9eb0ab5bd | |||
| 4bdb9fd469 | |||
| 38f700c3d7 | |||
|
|
1a1d55bc2e | ||
| a2f1d5ea7f | |||
|
|
9a65071276 | ||
| 12d6fa4d73 | |||
|
|
640b240ada | ||
|
|
f8fbbb7ac6 | ||
|
|
0e2789c209 | ||
| 7661862564 | |||
|
|
a2143fe1c5 | ||
|
|
494e3d93ab | ||
| 9afb079786 | |||
| 25958bb966 | |||
|
|
1d51370b3e | ||
| e44721b7be | |||
| 7ef057dc1f | |||
| 1b68a81204 | |||
| f77da50d1f | |||
| 79c2982e19 | |||
| fb4d166c31 | |||
| 9084f8fafb | |||
| b216a363fb | |||
|
|
b2391751f0 | ||
| df6d2bd04d | |||
| f277d87dfa | |||
| 672ec25438 | |||
| aa871c86f6 | |||
| 553a34054c | |||
| 8af152abc0 | |||
| 0446c83b36 | |||
| 255cfd3eb3 | |||
| b1be5b1e09 | |||
| 7d3565ec51 | |||
| fc599acf4a | |||
|
|
b40270f058 | ||
|
|
8d6fa3daac | ||
| 64dfd1ca8a | |||
| 665ed9e70f |
100
.env.example
Normal file
100
.env.example
Normal 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
43
.gitignore
vendored
@@ -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
142
.woodpecker.yml
Normal 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
10
LICENSE
@@ -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/>
|
||||
@@ -1,2 +1,5 @@
|
||||
# api
|
||||
# YARA724 API
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Fanavaran integration](docs/fanavaran/README.md)
|
||||
|
||||
BIN
assets/Vazirmatn-Bold.ttf
Normal file
BIN
assets/Vazirmatn-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/Vazirmatn-Regular.ttf
Normal file
BIN
assets/Vazirmatn-Regular.ttf
Normal file
Binary file not shown.
103
docs/architecture.md
Normal file
103
docs/architecture.md
Normal file
@@ -0,0 +1,103 @@
|
||||
docs/
|
||||
│
|
||||
├── README.md
|
||||
│
|
||||
├── architecture/
|
||||
│ ├── overview.md
|
||||
│ ├── principles.md
|
||||
│ ├── layers.md
|
||||
│ ├── modules.md
|
||||
│ ├── workflow-engine.md
|
||||
│ ├── event-driven.md
|
||||
│ ├── database.md
|
||||
│ ├── caching.md
|
||||
│ ├── authentication.md
|
||||
│ ├── authorization.md
|
||||
│ ├── file-storage.md
|
||||
│ ├── id-generation.md
|
||||
│ ├── error-handling.md
|
||||
│ ├── logging.md
|
||||
│ └── diagrams/
|
||||
│
|
||||
├── adr/
|
||||
│ ├── 0001-use-mongodb.md
|
||||
│ ├── 0002-use-workflow-engine.md
|
||||
│ ├── ...
|
||||
│
|
||||
├── engineering/
|
||||
│ ├── structure.md
|
||||
│ ├── coding-style.md
|
||||
│ ├── naming.md
|
||||
│ ├── comments.md
|
||||
│ ├── exceptions.md
|
||||
│ ├── validation.md
|
||||
│ ├── dto-guidelines.md
|
||||
│ ├── repositories.md
|
||||
│ ├── services.md
|
||||
│ ├── controllers.md
|
||||
│ ├── testing.md
|
||||
│ ├── code-review.md
|
||||
│ ├── gitflow.md
|
||||
│ ├── commit-convention.md
|
||||
│ ├── branching.md
|
||||
│ ├── dependency-rules.md
|
||||
│ ├── security.md
|
||||
│ └── performance.md
|
||||
│
|
||||
├── domain/
|
||||
│ ├── glossary.md
|
||||
│ ├── insurance-concepts.md
|
||||
│ ├── entities.md
|
||||
│ ├── events.md
|
||||
│ ├── workflows.md
|
||||
│ ├── business-rules.md
|
||||
│ └── state-transitions.md
|
||||
│
|
||||
├── flows/
|
||||
│ ├── company-a/
|
||||
│ ├── company-b/
|
||||
│ ├── company-c/
|
||||
│ └── common/
|
||||
│
|
||||
├── api/
|
||||
│ ├── rest.md
|
||||
│ ├── versioning.md
|
||||
│ ├── pagination.md
|
||||
│ ├── errors.md
|
||||
│ └── examples/
|
||||
│
|
||||
├── deployment/
|
||||
│ ├── docker.md
|
||||
│ ├── environments.md
|
||||
│ ├── ci.md
|
||||
│ ├── cd.md
|
||||
│ ├── backups.md
|
||||
│ └── monitoring.md
|
||||
│
|
||||
├── onboarding/
|
||||
│ ├── setup.md
|
||||
│ ├── first-day.md
|
||||
│ ├── debugging.md
|
||||
│ ├── faq.md
|
||||
│ └── common-mistakes.md
|
||||
│
|
||||
├── operations/
|
||||
│ ├── runbooks.md
|
||||
│ ├── incident-response.md
|
||||
│ ├── rca/
|
||||
│ ├── postmortems/
|
||||
│ └── troubleshooting.md
|
||||
│
|
||||
├── backlog/
|
||||
│ ├── ideas.md
|
||||
│ ├── technical-debt.md
|
||||
│ ├── future-features.md
|
||||
│ └── experiments.md
|
||||
│
|
||||
├── decisions/
|
||||
│ ├── rejected-ideas.md
|
||||
│ ├── deprecated.md
|
||||
│ └── migration-plans.md
|
||||
│
|
||||
├── changelog.md
|
||||
└── roadmap.md
|
||||
1506
docs/blame-claim-flow-architecture.fa.html
Normal file
1506
docs/blame-claim-flow-architecture.fa.html
Normal file
File diff suppressed because it is too large
Load Diff
1896
docs/blame-claim-flow-architecture.html
Normal file
1896
docs/blame-claim-flow-architecture.html
Normal file
File diff suppressed because it is too large
Load Diff
27
docs/documening-fanavaran-flow.md
Normal file
27
docs/documening-fanavaran-flow.md
Normal 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`.
|
||||
228
docs/expert-claim-reply-submit-v2-frontend.fa.md
Normal file
228
docs/expert-claim-reply-submit-v2-frontend.fa.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# راهنمای فرانتاند: ثبت پاسخ کارشناس خسارت (V2)
|
||||
|
||||
این مستند قرارداد API زیر را توضیح میدهد، بهویژه اعتبارسنجی مبلغها و ساختار خطاهایی که باید در فرم نمایش داده شوند.
|
||||
|
||||
```http
|
||||
PUT /v2/expert-claim/reply/submit/:claimRequestId
|
||||
Authorization: Bearer <actor-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
اگر محیط شما API را زیر پیشوند `/api` منتشر میکند، مسیر نهایی بهصورت `/api/v2/expert-claim/reply/submit/:claimRequestId` است.
|
||||
|
||||
## پیشنیازها
|
||||
|
||||
- کاربر باید نقش کارشناس مجاز داشته باشد.
|
||||
- پرونده باید پیش از ارسال توسط همین کارشناس قفل شده باشد.
|
||||
- `partId` هر ردیف باید از `damagedParts[].partId` در جزئیات پرونده انتخاب شود؛ شناسه دلخواه ارسال نکنید.
|
||||
- دستکم یک ردیف در `parts` لازم است.
|
||||
|
||||
## بدنه درخواست
|
||||
|
||||
تمام مبلغها باید **string** باشند. ارقام فارسی/انگلیسی و جداکننده هزارگان پذیرفته میشوند؛ برای نمونه هر دو مقدار `"100000"` و `"۱۰۰,۰۰۰"` معتبرند.
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "تعویض درب جلو لازم است",
|
||||
"parts": [
|
||||
{
|
||||
"partId": 201,
|
||||
"typeOfDamage": "تعویض",
|
||||
"price": "2500000",
|
||||
"salary": "400000",
|
||||
"totalPayment": "2900000",
|
||||
"factorNeeded": false,
|
||||
"daghi": {
|
||||
"option": "ارزش لوازم بازیافتی",
|
||||
"price": "300000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### فیلدهای سطح بالا
|
||||
|
||||
| فیلد | الزامی | توضیح |
|
||||
| --- | --- | --- |
|
||||
| `description` | خیر | یادداشت متنی کارشناس. میتواند ارسال نشود. |
|
||||
| `parts` | بله | آرایهای با حداقل یک ردیف قیمتگذاری. |
|
||||
|
||||
### فیلدهای هر ردیف `parts[i]`
|
||||
|
||||
| فیلد | الزامی | مقدار و قاعده |
|
||||
| --- | --- | --- |
|
||||
| `partId` | بله | عدد صحیح؛ باید متعلق به قطعات آسیبدیده همان پرونده باشد. |
|
||||
| `typeOfDamage` | بله | دقیقاً یکی از `"تعمیر"` یا `"تعویض"`. |
|
||||
| `price` | برای `"تعویض"` بله؛ برای `"تعمیر"` اختیاری | در صورت ارسال، مبلغ بین 100,000 تا 10,000,000,000 تومان. |
|
||||
| `salary` | بله | مبلغ بین 100,000 تا 10,000,000,000 تومان. |
|
||||
| `totalPayment` | بله | مبلغ بین 100,000 تا 10,000,000,000 تومان. مجموع این فیلدها در کل پرونده نباید از 53,000,000 تومان بیشتر شود. |
|
||||
| `factorNeeded` | بله | مقدار Boolean واقعی (`true` یا `false`)؛ رشته ارسال نکنید. |
|
||||
| `daghi` | برای `"تعویض"` بله | برای `"تعمیر"` لازم نیست و در ثبت نهایی حذف میشود. |
|
||||
| `daghi.option` | در صورت وجود `daghi` بله | یکی از `"ارزش لوازم بازیافتی"`، `"تحویل داغی"`، `"فاقد ارزش"` یا `"با احتساب داغی"`. |
|
||||
| `daghi.price` | فقط وقتی `option` برابر `"ارزش لوازم بازیافتی"` است | مبلغ بین 100,000 تا 10,000,000,000 تومان. |
|
||||
| `daghi.branchId` | فقط وقتی `option` برابر `"تحویل داغی"` است | Mongo ObjectId معتبرِ شعبه. |
|
||||
|
||||
نکتهها:
|
||||
|
||||
- مقدار `0` برای هیچ مبلغ ارسالی این endpoint معتبر نیست؛ حداقل مبلغ 100,000 تومان است.
|
||||
- محدودیت 10 میلیارد مربوط به **هر فیلد مبلغ** است؛ سقف 53 میلیون مربوط به **مجموع `totalPayment` تمام ردیفها** است و همچنان اعمال میشود.
|
||||
- فیلدهای ناشناخته در body حذف میشوند. فرانتاند نباید برای انتقال داده به آنها تکیه کند.
|
||||
|
||||
## پاسخ موفق
|
||||
|
||||
پاسخ `200` شامل مسیر بعدی workflow است. نمونه:
|
||||
|
||||
```json
|
||||
{
|
||||
"claimRequestId": "66c...",
|
||||
"status": "COMPLETED",
|
||||
"claimStatus": "APPROVED",
|
||||
"currentStep": "CLAIM_COMPLETED",
|
||||
"workflowNextStep": "CLAIM_COMPLETED",
|
||||
"factorNeeded": false,
|
||||
"mixedPricingAndFactors": false,
|
||||
"allPartsFactorNeeded": false,
|
||||
"isFinalReplyAfterObjection": false
|
||||
}
|
||||
```
|
||||
|
||||
مقادیر `status` و step به این وابستهاند که ردیف factor داشته باشید یا نه؛ فرانتاند باید از مقادیر پاسخ استفاده کند، نه اینکه مسیر بعدی را فقط از payload حدس بزند.
|
||||
|
||||
## قرارداد خطا
|
||||
|
||||
همه خطاهای این endpoint JSON هستند و NestJS مقدار `statusCode` را نیز به پاسخ اضافه میکند. برای نمایش پیام به کاربر از `message` استفاده کنید و برای منطق برنامه از `code` استفاده کنید؛ متن فارسی را با متن ثابت در فرانتاند جایگزین نکنید.
|
||||
|
||||
### 1. خطای ساختار DTO — `400`
|
||||
|
||||
برای نبودن فیلد الزامی، نوع اشتباه، enum نامعتبر، آرایه خالی، یا فرمت مبلغ نامعتبر، پاسخ زیر برمیگردد:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"message": "اطلاعات ارسالی پاسخ کارشناسی معتبر نیست.",
|
||||
"error": "EXPERT_REPLY_VALIDATION_ERROR",
|
||||
"code": "EXPERT_REPLY_VALIDATION_ERROR",
|
||||
"validationErrors": [
|
||||
{
|
||||
"field": "parts[0].salary",
|
||||
"message": "باید مبلغ صحیح و غیرمنفی به تومان باشد."
|
||||
},
|
||||
{
|
||||
"field": "parts[0].factorNeeded",
|
||||
"message": "باید درست یا نادرست باشد."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`validationErrors` ممکن است چند خطا داشته باشد. کلید `field` دقیقاً برای اتصال به کنترل فرم است؛ مانند `parts[0].daghi.price` یا `parts[2].partId`.
|
||||
|
||||
### 2. خطای قواعد مبلغ و قیمتگذاری — `400`
|
||||
|
||||
این خطاها پس از اعتبارسنجی ساختار و پیش از تغییر وضعیت پرونده بررسی میشوند. برای مثال، مبلغ کمتر از حداقل:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"message": "قیمت قطعه 201 باید حداقل ۱۰۰٬۰۰۰ و حداکثر ۱۰٬۰۰۰٬۰۰۰٬۰۰۰ تومان باشد.",
|
||||
"error": "EXPERT_REPLY_VALIDATION_ERROR",
|
||||
"code": "EXPERT_REPLY_VALIDATION_ERROR",
|
||||
"field": "parts[0].price",
|
||||
"partId": "201",
|
||||
"rule": "amount_out_of_range",
|
||||
"minAmount": 100000,
|
||||
"maxAmount": 10000000000
|
||||
}
|
||||
```
|
||||
|
||||
`rule` یکی از این مقادیر است:
|
||||
|
||||
| مقدار | معنی |
|
||||
| --- | --- |
|
||||
| `required` | فیلد لازم ارسال نشده است. |
|
||||
| `invalid_value` | مقدار enum یا شناسه معتبر نیست. |
|
||||
| `invalid_amount` | مبلغ، عدد صحیح غیرمنفی به تومان نیست. |
|
||||
| `amount_out_of_range` | مبلغ خارج از بازه `minAmount` و `maxAmount` است. |
|
||||
|
||||
در صورت `amount_out_of_range`، برای نمایش محدوده از اعداد `minAmount` و `maxAmount` استفاده کنید، نه از parse کردن متن `message`.
|
||||
|
||||
### 3. سقف مجموع مبلغها — `400`
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"message": "مجموع مبلغ قطعات (۵۴٬۰۰۰٬۰۰۰) از سقف مجاز (۵۳٬۰۰۰٬۰۰۰) تومان بیشتر است.",
|
||||
"error": "PRICE_CAP_ERROR",
|
||||
"code": "PRICE_CAP_ERROR",
|
||||
"totalPrice": 54000000,
|
||||
"priceCap": 53000000
|
||||
}
|
||||
```
|
||||
|
||||
این خطا به یک ردیف مشخص وصل نیست. آن را در بالای جدول قیمتها نمایش دهید و در صورت نیاز از `priceCap` برای پیام UI استفاده کنید.
|
||||
|
||||
### 4. خطاهای قواعد پرونده و workflow
|
||||
|
||||
این خطاها ساختار مشترک زیر را دارند:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"message": "قطعه با شناسه 201 در فهرست قطعات آسیبدیده پرونده وجود ندارد.",
|
||||
"error": "EXPERT_REPLY_SUBMISSION_ERROR",
|
||||
"code": "PART_NOT_ON_CLAIM",
|
||||
"field": "partId",
|
||||
"partId": 201
|
||||
}
|
||||
```
|
||||
|
||||
کدهای مهم:
|
||||
|
||||
| HTTP | `code` | رفتار پیشنهادی فرانتاند |
|
||||
| --- | --- | --- |
|
||||
| 404 | `CLAIM_NOT_FOUND` | پیام خطا و بازگشت به فهرست پروندهها. |
|
||||
| 400 | `CLAIM_NOT_REVIEWABLE` | جزئیات پرونده را refresh کنید؛ ثبت در وضعیت فعلی مجاز نیست. |
|
||||
| 403 | `CLAIM_NOT_LOCKED` | کاربر باید ابتدا پرونده را قفل کند. |
|
||||
| 403 | `CLAIM_LOCKED_BY_ANOTHER_EXPERT` | فرم را read-only کنید و پیام مناسب نمایش دهید. |
|
||||
| 400 | `PART_NOT_ON_CLAIM` | داده قطعات را refresh کنید و ردیف خطادار را اصلاح/حذف کنید. |
|
||||
| 400 | `PART_INVALID` / `PART_ID_INVALID` | ردیف دارای `partId` را اصلاح کنید. |
|
||||
| 400 | `DUPLICATE_PART` | ردیف تکراری را حذف کنید. |
|
||||
| 400 | `DAGHI_OPTION_REQUIRED` | فیلد `daghi.option` همان ردیف را نشاندار کنید. |
|
||||
| 400 | `DAGHI_PRICE_REQUIRED` | فیلد `daghi.price` همان ردیف را نشاندار کنید. |
|
||||
| 400 | `DAGHI_BRANCH_REQUIRED` / `DAGHI_BRANCH_INVALID` | فیلد `daghi.branchId` همان ردیف را نشاندار کنید. |
|
||||
| 409 | `FINAL_REPLY_ALREADY_SUBMITTED` | ثبت مجدد ممنوع است؛ داده پرونده را refresh کنید. |
|
||||
|
||||
برای خطاهای داغی که از سرویس برمیگردند، `field` به شکل `parts[<index>].daghi.<property>` است.
|
||||
|
||||
## الگوی پیشنهادی هندل کردن خطا در فرانتاند
|
||||
|
||||
```ts
|
||||
type ApiErrorBody = {
|
||||
message?: string;
|
||||
code?: string;
|
||||
field?: string;
|
||||
validationErrors?: Array<{ field: string; message: string }>;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
priceCap?: number;
|
||||
};
|
||||
|
||||
function applySubmitError(body: ApiErrorBody) {
|
||||
if (body.validationErrors?.length) {
|
||||
for (const issue of body.validationErrors) {
|
||||
setFieldError(issue.field, issue.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.field) {
|
||||
setFieldError(body.field, body.message ?? "مقدار واردشده معتبر نیست.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFormError(body.message ?? "ثبت پاسخ کارشناسی انجام نشد.");
|
||||
}
|
||||
```
|
||||
|
||||
قبل از ارسال، فرانتاند میتواند همین بازه مبلغ را برای تجربه کاربری بهتر بررسی کند؛ با این حال اعتبار نهایی همیشه پاسخ API است. برای جلوگیری از خطای سقف مجموع، جمع `totalPayment` ردیفها را نیز پیش از ارسال محاسبه و نمایش دهید.
|
||||
742
docs/external-api-curls.md
Normal file
742
docs/external-api-curls.md
Normal 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"
|
||||
}'
|
||||
```
|
||||
596
docs/external-integrations-reference.fa.html
Normal file
596
docs/external-integrations-reference.fa.html
Normal 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>
|
||||
593
docs/external-integrations-reference.html
Normal file
593
docs/external-integrations-reference.html
Normal 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 & 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>
|
||||
345
docs/fanavaran/01-write-apis.md
Normal file
345
docs/fanavaran/01-write-apis.md
Normal 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`.
|
||||
191
docs/fanavaran/02-read-apis-lookups.md
Normal file
191
docs/fanavaran/02-read-apis-lookups.md
Normal 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)).
|
||||
111
docs/fanavaran/03-constants.md
Normal file
111
docs/fanavaran/03-constants.md
Normal 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).
|
||||
63
docs/fanavaran/04-tenant-matrix.md
Normal file
63
docs/fanavaran/04-tenant-matrix.md
Normal 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).
|
||||
130
docs/fanavaran/05-claim-flow.md
Normal file
130
docs/fanavaran/05-claim-flow.md
Normal 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`.
|
||||
140
docs/fanavaran/06-yara-integration.md
Normal file
140
docs/fanavaran/06-yara-integration.md
Normal 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 |
|
||||
92
docs/fanavaran/07-data-mapping.md
Normal file
92
docs/fanavaran/07-data-mapping.md
Normal 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.
|
||||
45
docs/fanavaran/08-dependencies.md
Normal file
45
docs/fanavaran/08-dependencies.md
Normal 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.
|
||||
72
docs/fanavaran/09-error-handling.md
Normal file
72
docs/fanavaran/09-error-handling.md
Normal 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.
|
||||
180
docs/fanavaran/10-testing.md
Normal file
180
docs/fanavaran/10-testing.md
Normal 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).
|
||||
76
docs/fanavaran/11-onboard-new-client.md
Normal file
76
docs/fanavaran/11-onboard-new-client.md
Normal 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
101
docs/fanavaran/README.md
Normal 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.
|
||||
237
docs/inquiry-participants-frontend-guide.fa.md
Normal file
237
docs/inquiry-participants-frontend-guide.fa.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# راهنمای فرانتاند برای ارسال اطلاعات استعلام
|
||||
|
||||
این سند قرارداد نهایی فرانتاند برای مرحله استعلام است. از این به بعد اطلاعات اشخاص و خودرو باید با ساختار نقشمحور زیر ارسال شود. فیلدهای تخت قدیمی مانند `nationalCodeOfDriver` و `nationalCodeOfInsurer` دیگر ورودی معتبر نیستند.
|
||||
|
||||
## پاسخ کوتاه درباره `unknown`
|
||||
|
||||
`unknown` فقط برای یک حالت استثنایی لازم است: وقتی در مرحله طرف زیاندیده یک پرونده `THIRD_PARTY`، هویت بیمهگذار شخص ثالث واقعاً مشخص نیست.
|
||||
|
||||
```json
|
||||
{
|
||||
"thirdPartyPolicyholder": { "unknown": true }
|
||||
}
|
||||
```
|
||||
|
||||
قواعد آن:
|
||||
|
||||
- فقط برای طرف زیاندیده (`SECOND`) مجاز است؛ برای طرف مقصر (`FIRST`) خطا برمیگردد.
|
||||
- فقط برای `thirdPartyPolicyholder` مجاز است؛ برای راننده، مالک یا بیمهگذار بدنه مجاز نیست.
|
||||
- وقتی `unknown: true` ارسال میشود، هیچ فیلد هویتی دیگری در همان آبجکت نفرستید.
|
||||
- در استعلام شخص ثالث با پلاک یا VIN، استعلام بیمهگذار عمداً skip میشود و سیستم نباید کد ملی راننده یا مالک را جایگزین کند.
|
||||
- استعلامهای راننده، مالکیت خودرو و اطلاعات افراد شناختهشده همچنان اجرا میشوند.
|
||||
- اگر هویت بیمهگذار مشخص است، اصلاً از `unknown` استفاده نکنید و اطلاعات واقعی یا `sameAs` را بفرستید.
|
||||
|
||||
## ساختار کلی درخواست
|
||||
|
||||
برای پرونده `THIRD_PARTY`:
|
||||
|
||||
```json
|
||||
{
|
||||
"driver": {
|
||||
"nationalCode": "0012345678",
|
||||
"birthday": "1370/01/01",
|
||||
"hasDrivingLicense": true,
|
||||
"licenseNumber": "123456789",
|
||||
"licenseType": "1"
|
||||
},
|
||||
"vehicleOwner": { "sameAs": "DRIVER" },
|
||||
"thirdPartyPolicyholder": { "sameAs": "VEHICLE_OWNER" },
|
||||
"vehicle": {
|
||||
"registrationState": "CURRENT",
|
||||
"currentPlate": {
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
},
|
||||
"isNewCar": false
|
||||
},
|
||||
"sheba": "IR123456789012345678901234"
|
||||
}
|
||||
```
|
||||
|
||||
برای پرونده `CAR_BODY`، نقش بیمهگذار بدنه هم الزامی است:
|
||||
|
||||
```json
|
||||
{
|
||||
"driver": {
|
||||
"nationalCode": "0012345678",
|
||||
"birthday": "1370/01/01",
|
||||
"hasDrivingLicense": false
|
||||
},
|
||||
"vehicleOwner": { "sameAs": "DRIVER" },
|
||||
"thirdPartyPolicyholder": {
|
||||
"nationalCode": "0023456789",
|
||||
"birthday": "1360/02/02"
|
||||
},
|
||||
"carBodyPolicyholder": {
|
||||
"nationalCode": "0034567890",
|
||||
"birthday": "1350/03/03"
|
||||
},
|
||||
"vehicle": {
|
||||
"currentPlate": {
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
},
|
||||
"isNewCar": false
|
||||
},
|
||||
"sheba": "IR123456789012345678901234"
|
||||
}
|
||||
```
|
||||
|
||||
`sheba` فقط در routeهایی که قبلاً اطلاعات بانکی را در مرحله استعلام دریافت میکردند ارسال میشود؛ در V6 مرکز تماس، شماره شبا در این body نیست و بعداً توسط کاربر دریافت میشود.
|
||||
|
||||
## هر استعلام با اطلاعات کدام نقش انجام میشود؟
|
||||
|
||||
فرانتاند فقط اشخاص را با ساختار نقشمحور ارسال میکند؛ انتخاب کد ملی مناسب برای هر سرویس در بکاند انجام میشود:
|
||||
|
||||
| استعلام | کد ملی مورد استفاده | شناسه خودرو/بانکی |
|
||||
| --- | --- | --- |
|
||||
| بیمه شخص ثالث با پلاک یا VIN | `thirdPartyPolicyholder.nationalCode` | پلاک یا `vehicle.vin` |
|
||||
| بیمه بدنه با پلاک یا VIN | `carBodyPolicyholder.nationalCode` | پلاک یا `vehicle.vin` |
|
||||
| تطبیق شبا | `vehicleOwner.nationalCode` | `sheba` |
|
||||
| گواهینامه | `driver.nationalCode` | `driver.licenseNumber` |
|
||||
|
||||
بنابراین کد ملی راننده نباید بهجای بیمهگذار یا مالک ارسال یا تکرار شود. اگر چند نقش متعلق به یک نفر است، `sameAs` ارتباط را مشخص میکند و بکاند همان شخص را برای استعلام مربوط به هر نقش انتخاب میکند.
|
||||
|
||||
در مرحله بانکی بعدی جریان V6، برای پروندههای جدید فقط `sheba` لازم است. بکاند کد ملی مالک خودرو را از `participants` و `participantRoles` ذخیرهشده در پرونده تقصیر میخواند. فیلدهای قدیمی `nationalCodeOfInsurer` یا `nationalCodeOfOwner` فقط برای پروندههای تاریخی فاقد اطلاعات نقشمحور fallback هستند؛ اگر همراه پرونده جدید ارسال شوند باید با مالک ذخیرهشده یکسان باشند.
|
||||
|
||||
پنلهای کارشناسی اطلاعات کامل اشخاص را در `parties[].participants` و نگاشت نقشها را در `parties[].participantRoles` دریافت میکنند. اطلاعات راننده حتی اگر در استعلام بیمه یا شبا استفاده نشود در همین ساختار ذخیره و نمایش داده میشود.
|
||||
|
||||
## نقشها و فیلدهای هر شخص
|
||||
|
||||
هر نقش باید یکی از این دو حالت را داشته باشد، نه هر دو را:
|
||||
|
||||
1. اطلاعات یک شخص جدید؛ یا
|
||||
2. ارجاع با `sameAs` به شخصی که قبلاً در همین درخواست معرفی شده است.
|
||||
|
||||
| فیلد | کاربرد | وضعیت |
|
||||
| --- | --- | --- |
|
||||
| `nationalCode` | کد ملی شخص | برای شخص جدید الزامی |
|
||||
| `birthday` | تاریخ تولد جلالی | برای شخص جدید الزامی |
|
||||
| `fullName` | نام نمایشی شخص | اختیاری |
|
||||
| `sameAs` | اتصال این نقش به نقش دیگر | بهجای اطلاعات شخص جدید |
|
||||
| `unknown` | نامشخص بودن بیمهگذار ثالث | فقط `SECOND` در `THIRD_PARTY` |
|
||||
| `hasDrivingLicense` | داشتن گواهینامه راننده | برای نقش راننده الزامی |
|
||||
| `licenseNumber` | شماره گواهینامه | اگر `hasDrivingLicense=true` الزامی |
|
||||
| `licenseType` | نوع گواهینامه | اگر `hasDrivingLicense=true` الزامی |
|
||||
|
||||
مقادیر مجاز `sameAs` عبارتاند از:
|
||||
|
||||
- `DRIVER`
|
||||
- `VEHICLE_OWNER`
|
||||
- `THIRD_PARTY_POLICYHOLDER`
|
||||
- `CAR_BODY_POLICYHOLDER`
|
||||
|
||||
برای `sameAs` هیچکدام از `nationalCode`، `birthday`، `fullName`، اطلاعات گواهینامه یا `phoneNumber` را در همان آبجکت نفرستید.
|
||||
|
||||
شماره تلفن بخشی از هویت استعلام نیست و در این DTOها وجود ندارد. احراز هویت پیامکی و شماره تماس طرفین از این مرحله جداست.
|
||||
|
||||
## اطلاعات خودرو و پلاک
|
||||
|
||||
| فیلد | کاربرد | وضعیت |
|
||||
| --- | --- | --- |
|
||||
| `vehicle.registrationState` | وضعیت ثبت رسمی خودرو | `CURRENT` یا `RECENTLY_TRANSFERRED`؛ پیشفرض `CURRENT` |
|
||||
| `vehicle.currentPlate` | پلاک رسمی فعلی و شناسه اصلی خودرو | الزامی |
|
||||
| `vehicle.previousPlate` | پلاک قبلی در انتقال اخیر | فقط در `RECENTLY_TRANSFERRED` |
|
||||
| `vehicle.vin` | شماره شاسی/VIN | در انتقال اخیر الزامی؛ حداکثر ۱۷ کاراکتر |
|
||||
| `vehicle.isNewCar` | نو بودن خودرو | اختیاری |
|
||||
|
||||
اجزای پلاک:
|
||||
|
||||
```json
|
||||
{
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
}
|
||||
```
|
||||
|
||||
`leftDigits`، `centerDigits` و `ir` را میتوان بهصورت string یا number فرستاد؛ ارسال string پیشنهاد میشود تا صفرهای ابتدایی از بین نروند. `centerAlphabet` باید حرف فارسی پلاک باشد.
|
||||
|
||||
### انتقال اخیر
|
||||
|
||||
```json
|
||||
{
|
||||
"vehicle": {
|
||||
"registrationState": "RECENTLY_TRANSFERRED",
|
||||
"currentPlate": {
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
},
|
||||
"previousPlate": {
|
||||
"leftDigits": "55",
|
||||
"centerAlphabet": "ج",
|
||||
"centerDigits": "222",
|
||||
"ir": "33"
|
||||
},
|
||||
"vin": "NAAM01E15HK123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
سیستم ابتدا پلاک فعلی را استعلام میکند. اگر نتیجه ناموجود، منقضی یا فاقد بیمهنامه مرتبط باشد، پلاک قبلی را امتحان میکند. نتیجه پلاک قبلی فقط در صورت تطبیق VIN پذیرفته میشود؛ پلاک قبلی هرگز جایگزین پلاک فعلی نمیشود.
|
||||
|
||||
حتی در route مربوط به VIN، آبجکت `vehicle` از قرارداد مشترک استفاده میکند و `currentPlate` در قرارداد فعلی الزامی است. مقدار VIN در `vehicle.vin` قرار میگیرد، نه در فیلد سطح بالای `vin`.
|
||||
|
||||
## ترتیب پیشنهادی نمایش فرم
|
||||
|
||||
1. پلاک فعلی و وضعیت انتقال خودرو را بگیرید.
|
||||
2. اگر انتقال اخیر بود، پلاک قبلی و VIN را بگیرید.
|
||||
3. اطلاعات راننده و وضعیت گواهینامه را بگیرید.
|
||||
4. بپرسید مالک خودرو همان راننده است یا شخص دیگری؛ در حالت یکسان از `sameAs` استفاده کنید.
|
||||
5. بیمهگذار شخص ثالث را از بین راننده، مالک یا شخص دیگر انتخاب کنید.
|
||||
6. در `CAR_BODY` همین کار را برای بیمهگذار بدنه انجام دهید.
|
||||
7. خلاصه اطلاعات را به کاربر نشان دهید و سپس درخواست استعلام را ارسال کنید.
|
||||
|
||||
## مسیرهای اصلی
|
||||
|
||||
بدنه درخواست در همه این مسیرها همین ساختار را دارد:
|
||||
|
||||
| جریان | مسیر پلاک | مسیر VIN |
|
||||
| --- | --- | --- |
|
||||
| کاربر V2 | `/v2/blame-request-management/initial-form/:requestId` | `/v2/blame-request-management/initial-form-vin/:requestId` |
|
||||
| کارشناس/پروندهساز V3 تا V5 | `.../run-inquiries/:requestId` | `.../run-inquiries-vin/:requestId` |
|
||||
| مرکز تماس V6 | `/v6/call-center-blame/run-inquiry/:requestId` | `/v6/call-center-blame/run-inquiry-vin/:requestId` |
|
||||
|
||||
در جریانهای V3 تا V5، فراخوان اول برای طرف مقصر (`FIRST`) و فراخوان دوم، فقط در `THIRD_PARTY`، برای طرف زیاندیده (`SECOND`) است. همین تفاوت تعیین میکند که `unknown` مجاز است یا نه.
|
||||
|
||||
## فیلدهایی که نباید ارسال شوند
|
||||
|
||||
این فیلدها دیگر بخشی از قرارداد ورودی نیستند و ارسال آنها باعث خطای اعتبارسنجی میشود:
|
||||
|
||||
```text
|
||||
nationalCodeOfDriver
|
||||
driverBirthday
|
||||
driverLicense
|
||||
licenseType // در سطح بالا؛ مقدار صحیح داخل driver است
|
||||
nationalCodeOfInsurer
|
||||
insurerBirthday
|
||||
insurerLicense
|
||||
driverIsInsurer
|
||||
userNoCertificate
|
||||
plate // در سطح بالا؛ مقدار صحیح داخل vehicle.currentPlate است
|
||||
plateId
|
||||
vin // در سطح بالا؛ مقدار صحیح داخل vehicle.vin است
|
||||
isNewCar // در سطح بالا؛ مقدار صحیح داخل vehicle.isNewCar است
|
||||
phoneNumber // در participantها
|
||||
```
|
||||
|
||||
## خطاهای رایج فرانتاند
|
||||
|
||||
- ارسال `vehicleOwner` بهصورت خالی؛ باید شخص جدید یا `sameAs` باشد.
|
||||
- استفاده از `sameAs` همراه با `nationalCode` یا `birthday`.
|
||||
- ارسال `carBodyPolicyholder` برای `THIRD_PARTY`.
|
||||
- ارسال `unknown` برای طرف مقصر یا برای نقشی غیر از بیمهگذار ثالث.
|
||||
- فرستادن `previousPlate` بدون `registrationState=RECENTLY_TRANSFERRED`.
|
||||
- فرستادن `RECENTLY_TRANSFERRED` بدون `previousPlate` یا `vin`.
|
||||
- قرار دادن VIN یا پلاک در سطح بالای body.
|
||||
- ارسال شماره تلفن در آبجکت شخص.
|
||||
- تکرار کد ملی راننده یا بیمهگذار در مرحله شبا؛ تطبیق شبا همیشه با مالک خودرو انجام میشود.
|
||||
|
||||
مستند مدل دامنه و جزئیات تصمیم معماری در [inquiry-participants-proposal.fa.md](./inquiry-participants-proposal.fa.md) قرار دارد.
|
||||
116
docs/inquiry-participants-proposal.fa.md
Normal file
116
docs/inquiry-participants-proposal.fa.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# پیشنهاد مدل اشخاص در مرحله استعلام
|
||||
|
||||
وضعیت: پیادهسازیشده در ۱۴۰۵/۰۶/۲۲
|
||||
دامنه: تمام جریانهای استعلام کاربر، کارشناس، پروندهساز و مرکز تماس
|
||||
نسخه انگلیسی: [inquiry-participants-proposal.md](./inquiry-participants-proposal.md)
|
||||
|
||||
## interface پیادهسازیشده
|
||||
|
||||
فیلدهای نقشها و آبجکت الزامی `vehicle` که در ادامه آمدهاند، مستقیماً در body تمام درخواستهای استعلام فعلی پذیرفته میشوند. این تغییر شامل فرم اولیه کاربر و mirror کارشناس/ثبتکننده در V2، جریان کارشناس V3، جریانهای پروندهساز V4/V5، مرکز تماس V6 و مسیرهای تکدرخواستی حضوری است. routeهای پلاک و VIN از قوانین مشترک اشخاص استفاده میکنند. فیلدهای تخت راننده/بیمهگذار و شماره تلفن، ورودی استعلام نیستند.
|
||||
|
||||
پاسخها و جزئیات پرونده برای نقشهای عملیاتی، در صورت وجود داده، فیلدهای نرمالشده `participants`، `participantRoles`، `vehicle.registrationState` و `vehicle.previousPlateId` را نمایش میدهند.
|
||||
|
||||
## مسئله
|
||||
|
||||
قرارداد فعلی استعلام عمدتاً فقط راننده و شخصی با عنوان `insurer` را نگه میدارد. این مدل کامل نیست و نامگذاری نیز دقیق نیست: شخص، **بیمهگذار** است و **بیمهگر** شرکت بیمه است.
|
||||
|
||||
برای هر وسیله نقلیه در یک `Party` نقشهای هویتی زیر وجود دارد:
|
||||
|
||||
| نوع پرونده | نقشهای الزامی |
|
||||
| ------------- | ------------------------------------------------------------ |
|
||||
| `THIRD_PARTY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث |
|
||||
| `CAR_BODY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث، بیمهگذار بدنه |
|
||||
|
||||
ممکن است یک شخص چند نقش را داشته باشد، اما سیستم نباید یکسان بودن آنها را فرض کند.
|
||||
|
||||
## پیشنهاد
|
||||
|
||||
پیش از اجرای استعلام، یک **مرحله کوتاه تعیین اشخاص** اضافه شود. ابتدا نسبت اشخاص پرسیده شود و اطلاعات فقط برای افراد متفاوت دریافت شود. دریافت بدون شرط اطلاعات کامل همه اشخاص مناسب نیست. همچنین افزودن فلگهای دوتایی متعدد مانند `driverIsOwner` و `ownerIsPolicyholder` باعث ابهام، تناقض و رشد سریع حالتها میشود.
|
||||
|
||||
بهجای آن، هر نقش یا اطلاعات یک شخص جدید را داشته باشد یا به نقش قبلی ارجاع دهد:
|
||||
|
||||
```json
|
||||
{
|
||||
"driver": {
|
||||
"nationalCode": "0012345678",
|
||||
"birthday": "1370/01/01",
|
||||
"hasDrivingLicense": true,
|
||||
"licenseNumber": "123456789",
|
||||
"licenseType": "1"
|
||||
},
|
||||
"vehicleOwner": { "sameAs": "DRIVER" },
|
||||
"thirdPartyPolicyholder": { "sameAs": "VEHICLE_OWNER" },
|
||||
"carBodyPolicyholder": {
|
||||
"nationalCode": "0098765432",
|
||||
"birthday": "1365/02/03"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بکاند این ورودی را به فهرست اشخاص یکتا و اتصال نقشها به آنها تبدیل میکند.
|
||||
|
||||
فقط برای طرف زیاندیده در پرونده `THIRD_PARTY` میتوان نامشخص بودن بیمهگذار را بهصورت صریح با `"thirdPartyPolicyholder": { "unknown": true }` ارسال کرد. این حالت برای طرف مقصر پذیرفته نمیشود. استعلام بیمه شخص ثالث با پلاک یا VIN ــ چون هر دو به کد ملی بیمهگذار نیاز دارند ــ بهشکل قابلممیزی «عمداً اجرا نشد» ثبت میشود و بکاند نباید کد ملی راننده یا مالک را جایگزین کند. اعتبارسنجی راننده و مالک و استعلامهای هویت، مالکیت و گواهینامه همچنان انجام میشوند.
|
||||
|
||||
برای راننده، `hasDrivingLicense` الزامی است. اگر مقدار آن `true` باشد، هر دو فیلد `licenseNumber` و `licenseType` نیز الزامی هستند؛ اگر مقدار آن `false` باشد، استعلام گواهینامه عمداً اجرا نمیشود.
|
||||
|
||||
## انتقال مالکیت اخیر و پلاک قبلی
|
||||
|
||||
نقش اشخاص و شناسههای خودرو دو موضوع جدا هستند. اگر خودرو بهتازگی فروخته یا خریداری شده باشد، ممکن است اطلاعات رسمی یا بیمهنامه هنوز به پلاک قبلی متصل باشد. این وضعیت باید صریح ثبت شود و پلاک قبلی نباید جایگزین پلاک فعلی شود:
|
||||
|
||||
```json
|
||||
{
|
||||
"vehicle": {
|
||||
"registrationState": "RECENTLY_TRANSFERRED",
|
||||
"currentPlate": {
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
},
|
||||
"previousPlate": {
|
||||
"leftDigits": "55",
|
||||
"centerAlphabet": "ج",
|
||||
"centerDigits": "222",
|
||||
"ir": "33"
|
||||
},
|
||||
"vin": "NAAM01E15HK123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
مقدار پیشفرض `registrationState` برابر `CURRENT` است و برای این مسیر استثنایی مقدار `RECENTLY_TRANSFERRED` استفاده میشود. `previousPlate` فقط در حالت انتقال اخیر الزامی است. پلاک فعلی همچنان شناسه اصلی خودرو است. هماهنگکننده استعلام باید ابتدا پلاک فعلی را بررسی کند و اگر نتیجه ناموجود، قدیمی یا فاقد بیمهنامه مرتبط بود، پلاک قبلی را بهصورت خودکار استعلام کند.
|
||||
|
||||
پیش از پذیرش نتیجه پلاک قبلی، بکاند باید یکسان بودن VIN/شماره شاسی را بررسی کند. در صورت مغایرت، انتخاب خودکار متوقف و اصلاح اطلاعات یا بررسی دستی الزامی شود. هر دو پلاک و تمام تلاشهای استعلام برای ممیزی نگهداری شوند، اما پلاک قبلی هیچگاه نباید روی پلاک فعلی نوشته شود.
|
||||
|
||||
## ترتیب پیشنهادی فرم
|
||||
|
||||
1. پلاک فعلی دریافت و درباره انتقال مالکیت اخیر پرسیده شود. در صورت انتقال اخیر، پلاک قبلی و VIN/شماره شاسی نیز دریافت شوند.
|
||||
2. اطلاعات هویتی و گواهینامه راننده دریافت شود.
|
||||
3. پرسیده شود آیا مالک خودرو همان راننده است؛ فقط در صورت تفاوت، اطلاعات مالک دریافت شود.
|
||||
4. برای بیمهگذار شخص ثالث یکی از «راننده»، «مالک» یا «شخص دیگر» انتخاب شود؛ فقط برای شخص دیگر فرم جدید نمایش داده شود.
|
||||
5. در `CAR_BODY` همین انتخاب برای بیمهگذار بدنه انجام شود و امکان انتخاب هر شخص ثبتشده یا شخص دیگر وجود داشته باشد.
|
||||
6. خلاصه اشخاص نمایش داده شود و سپس استعلامها اجرا شوند.
|
||||
|
||||
به این ترتیب مسیر رایج کوتاه میماند و همه ترکیبهای معتبر نیز پشتیبانی میشوند.
|
||||
|
||||
## محل منطق در بکاند
|
||||
|
||||
یک resolver مشترک برای اشخاص ساخته شود و تمام routeهای استعلام از آن استفاده کنند. interface این ماژول باید:
|
||||
|
||||
- نقشهای لازم را بر اساس نوع پرونده اعتبارسنجی و ارجاعهای نامعتبر یا حلقوی `sameAs` را رد کند؛
|
||||
- شخص نهایی هر نقش را برگرداند؛
|
||||
- هویت درست را به استعلام مرتبط بدهد: گواهینامه ← راننده، مالکیت و تطبیق شبا ← مالک خودرو، بیمه شخص ثالث با پلاک/VIN ← بیمهگذار شخص ثالث، بیمه بدنه با پلاک/VIN ← بیمهگذار بدنه؛
|
||||
- بر اساس یک قاعده مشخص، پلاک فعلی یا قبلی را انتخاب و نتیجه پلاک قبلی را با VIN/شماره شاسی تطبیق دهد؛
|
||||
- استعلام هویت را برای هر شخص یکتا فقط یک بار اجرا کند؛
|
||||
- اشخاص نرمالشده و نقشهای آنها را در `Party` مربوط ذخیره کند.
|
||||
- `participants` و `participantRoles` ذخیرهشده را بدون حذف اطلاعات در جزئیات پرونده پنلهای کارشناسی و پرونده خسارت متصل نمایش دهد تا اطلاعات راننده و سایر نقشها برای بررسی در دسترس بماند.
|
||||
|
||||
جریانهای V2 کاربر/کارشناس، V3، V4، V5 و V6 باید adapter همین قوانین مشترک باشند و منطق نسبت اشخاص را جداگانه پیادهسازی نکنند.
|
||||
|
||||
## مرز قرارداد
|
||||
|
||||
ورودی استعلام فقط شامل آبجکتهای ساختاریافته اشخاص و خودرو است. بکاند فیلدهای تختی مانند `nationalCodeOfDriver`، `nationalCodeOfInsurer`، `driverIsInsurer`، `plate`/`vin` سطح بالا و `phoneNumber` را با خطای اعتبارسنجی رد میکند. احراز هویت تلفنی و جریانهای تماس با طرفین از جمعآوری هویت برای استعلام جدا هستند.
|
||||
|
||||
## تصمیم پیشنهادی
|
||||
|
||||
راهحل مناسب، **دریافت شرطی اطلاعات همراه با اتصال صریح نقشها** است. این روش بدون طولانی کردن مسیر اکثر کاربران، اطلاعات کامل فراهم میکند، از تناقض فلگها جلوگیری میکند و یک مدل یکسان برای همه جریانهای استعلام میسازد.
|
||||
116
docs/inquiry-participants-proposal.md
Normal file
116
docs/inquiry-participants-proposal.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Inquiry participant identity proposal
|
||||
|
||||
Status: implemented on 2026-09-13
|
||||
Scope: every user, expert, FileMaker, and call-center inquiry flow
|
||||
Persian version: [inquiry-participants-proposal.fa.md](./inquiry-participants-proposal.fa.md)
|
||||
|
||||
## Implemented interface
|
||||
|
||||
The role fields and required `vehicle` object shown below are accepted directly in every existing inquiry request body. This covers V2 user and expert/registrar mirror initial forms, V3 expert flow, V4/V5 FileMaker flows, V6 call-center flow, and the one-shot in-person completion paths. Plate and VIN routes share the same participant rules. Flat driver/insurer fields and phone numbers are not inquiry inputs.
|
||||
|
||||
Responses and file-detail views for operational actors expose normalized `participants`, `participantRoles`, `vehicle.registrationState`, and `vehicle.previousPlateId` where available.
|
||||
|
||||
## Problem
|
||||
|
||||
The current inquiry contract mainly models a driver and a value named `insurer`. That is incomplete and the name is misleading: a person is the **policyholder**; the **insurer** is the insurance company.
|
||||
|
||||
Each vehicle-side `Party` can have these identity roles:
|
||||
|
||||
| Case type | Required roles |
|
||||
| ------------- | ---------------------------------------------------------------------- |
|
||||
| `THIRD_PARTY` | Driver, vehicle owner, third-party policyholder |
|
||||
| `CAR_BODY` | Driver, vehicle owner, third-party policyholder, car-body policyholder |
|
||||
|
||||
One person may hold several roles, but the system must not assume that they do.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Add a short **participant-identification step before inquiry**. Ask relationship questions and collect details only for distinct people. Do not ask for every person's complete data unconditionally, and do not add pairwise flags such as `driverIsOwner`, `ownerIsPolicyholder`, and `driverIsBodyPolicyholder`; that becomes ambiguous and grows combinatorially.
|
||||
|
||||
Use explicit role references instead:
|
||||
|
||||
```json
|
||||
{
|
||||
"driver": {
|
||||
"nationalCode": "0012345678",
|
||||
"birthday": "1370/01/01",
|
||||
"hasDrivingLicense": true,
|
||||
"licenseNumber": "123456789",
|
||||
"licenseType": "1"
|
||||
},
|
||||
"vehicleOwner": { "sameAs": "DRIVER" },
|
||||
"thirdPartyPolicyholder": { "sameAs": "VEHICLE_OWNER" },
|
||||
"carBodyPolicyholder": {
|
||||
"nationalCode": "0098765432",
|
||||
"birthday": "1365/02/03"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
|
||||
|
||||
For the damaged party of a `THIRD_PARTY` case only, the policyholder can be explicitly unresolved with `"thirdPartyPolicyholder": { "unknown": true }`. This is not accepted for the guilty party. Third-party-policy inquiry by either plate or VIN is recorded as intentionally skipped because both routes require the policyholder national code; the backend must not substitute the driver or owner. The driver, owner, personal, ownership, and licence rules remain enforced.
|
||||
|
||||
For Driver, `hasDrivingLicense` is required. When it is `true`, both `licenseNumber` and `licenseType` are required; when it is `false`, the licence inquiry is intentionally skipped.
|
||||
|
||||
## Recent ownership transfer and previous plate
|
||||
|
||||
Participant roles and vehicle identifiers are separate concerns. When a vehicle has recently been sold or purchased, the current official record or policy may still be connected to its previous plate. Model this explicitly instead of replacing the current plate:
|
||||
|
||||
```json
|
||||
{
|
||||
"vehicle": {
|
||||
"registrationState": "RECENTLY_TRANSFERRED",
|
||||
"currentPlate": {
|
||||
"leftDigits": "44",
|
||||
"centerAlphabet": "ب",
|
||||
"centerDigits": "111",
|
||||
"ir": "22"
|
||||
},
|
||||
"previousPlate": {
|
||||
"leftDigits": "55",
|
||||
"centerAlphabet": "ج",
|
||||
"centerDigits": "222",
|
||||
"ir": "33"
|
||||
},
|
||||
"vin": "NAAM01E15HK123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`registrationState` is `CURRENT` by default or `RECENTLY_TRANSFERRED` for this exceptional path. `previousPlate` is required only when `registrationState=RECENTLY_TRANSFERRED`. The current plate remains the vehicle's primary identifier. The inquiry orchestrator should query the current plate first and automatically try the previous plate when the current result is missing, stale, or does not find the relevant policy.
|
||||
|
||||
Before accepting a previous-plate result, the backend must correlate it to the same VIN/chassis. A mismatch must stop automatic selection and require correction or manual review. Both identifiers and every attempted inquiry should be retained for audit, but a previous plate must never overwrite the current plate.
|
||||
|
||||
## Suggested UI sequence
|
||||
|
||||
1. Collect the current plate and ask whether the vehicle was recently transferred. If yes, collect the previous plate and VIN/chassis.
|
||||
2. Collect driver identity and licence details.
|
||||
3. Ask whether the vehicle owner is the driver; collect owner identity only when different.
|
||||
4. Ask whether the third-party policyholder is the driver, the owner, or another person; collect identity only for “another person”.
|
||||
5. For `CAR_BODY`, ask the same question for the car-body policyholder, allowing any already entered person or another person.
|
||||
6. Show a short review, then run the inquiries.
|
||||
|
||||
This keeps the common case fast while representing all valid combinations.
|
||||
|
||||
## Backend seam
|
||||
|
||||
Create one shared participant resolver used by every inquiry route. Its interface should:
|
||||
|
||||
- validate required roles by case type and reject circular/invalid `sameAs` references;
|
||||
- return the resolved person for each role;
|
||||
- route the correct identity to each inquiry: driver licence → Driver, ownership and Sheba validation → Vehicle Owner, third-party policy by plate/VIN → Third-party Policyholder, car-body policy by plate/VIN → Car-body Policyholder;
|
||||
- choose the current or previous plate deterministically and verify previous-plate results against VIN/chassis;
|
||||
- run personal identity inquiry once per distinct person;
|
||||
- persist normalized participants and role assignments on the relevant `Party`.
|
||||
- expose the persisted `participants` and `participantRoles` unchanged in expert-facing blame and linked-claim details so driver and other role data remain available for review.
|
||||
|
||||
V2 user/expert routes, V3, V4, V5, and V6 should be adapters over this shared rule set rather than implementing their own relationship logic.
|
||||
|
||||
## Contract boundary
|
||||
|
||||
The structured participant and vehicle objects are the only accepted inquiry input. The backend rejects flat fields such as `nationalCodeOfDriver`, `nationalCodeOfInsurer`, `driverIsInsurer`, top-level `plate`/`vin`, and `phoneNumber` with a validation error. Phone-based authentication and party contact flows remain separate from inquiry identity collection.
|
||||
|
||||
## Decision
|
||||
|
||||
Prefer **conditional collection plus explicit role assignments**. It provides complete data without burdening most users, prevents contradictory booleans, and gives all inquiry flows one consistent domain model.
|
||||
26
docs/panel-data-frontend-handoff.fa.md
Normal file
26
docs/panel-data-frontend-handoff.fa.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# اطلاعات پنلها و PDF
|
||||
|
||||
## مواردی که نبود و اضافه شد
|
||||
|
||||
- نام شخص در پنلهای فلو ۴ و ۵
|
||||
- کد ملی بیمهگذار و راننده در پنلهای فلو ۴ و ۵
|
||||
- مشخصبودن یکسانبودن راننده و بیمهگذار در فلو ۴ و ۵
|
||||
- نوع، شماره و تاریخ گواهینامه در پنلهای فلو ۴ و ۵
|
||||
- تاریخ تولد بیمهگذار و راننده در پنلهای فلو ۴ و ۵
|
||||
- پلاک و VIN در پنلهای فلو ۴ و ۵
|
||||
- شماره شبا در پنلهای فلو ۴ و ۵
|
||||
- کدهای تکمیلی فناوران در پنلها: بیمهنامه، راننده، نوع خودرو، نوع/نمونه پلاک، کاربری خودرو و شرکت بیمه
|
||||
- نمایش نوع واقعی گواهینامه، مثل «پایه یک»، در PDF
|
||||
|
||||
## مواردی که از قبل وجود داشت
|
||||
|
||||
- نام و کد ملی افراد در پنلهای کارشناس و بیمهگر
|
||||
- کد ملی راننده، در صورت متفاوتبودن با بیمهگذار
|
||||
- شماره شبا در پنل بیمهگر و PDF
|
||||
- اطلاعات راننده در PDF، وقتی راننده با بیمهگذار متفاوت است
|
||||
- پلاک خودرو در API؛ فرانت باید از `vehicle.plateId` بخواند
|
||||
- کدهای اصلی فناوران: شماره خسارت، شماره پرونده، شناسه پرونده خسارت و شناسه کارشناسی
|
||||
|
||||
## موردی که هنوز نداریم
|
||||
|
||||
- `expedited` → در بکاند فیلد و قرارداد API ندارد.
|
||||
625
docs/panel-roles-reference.fa.html
Normal file
625
docs/panel-roles-reference.fa.html
Normal 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>
|
||||
617
docs/panel-roles-reference.html
Normal file
617
docs/panel-roles-reference.html
Normal 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 & 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 and daghi). <code>daghi.branchId</code> is used only with the <code>تحویل داغی</code> option. 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 <code>daghi.branchId</code> when the expert selects the <code>تحویل داغی</code> option.</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>
|
||||
402
docs/report-api-frontend-fa.md
Normal file
402
docs/report-api-frontend-fa.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# مستند فرانتاند API گزارش PDF پرونده
|
||||
|
||||
## هدف API
|
||||
این API دادهی ساختیافتهی لازم برای تولید PDF پرونده در پنل بیمهگر را برمیگرداند.
|
||||
خروجی آن ترکیبی از اطلاعات پروندهی تقصیر (`blame`) و پروندهی خسارت (`claim`) است و طوری طراحی شده که فرانتاند بدون وابستگی به مدلهای داخلی بکاند، فقط با `sections` و `fields` بتواند PDF را رندر کند.
|
||||
|
||||
---
|
||||
|
||||
## آدرس API
|
||||
|
||||
```http
|
||||
GET /expert-insurer/files/:publicId/report
|
||||
```
|
||||
|
||||
### پارامتر مسیر
|
||||
- `publicId`: شناسه عمومی پرونده
|
||||
|
||||
---
|
||||
|
||||
## ساختار کلی پاسخ
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "گزارش پرونده بیمه گر",
|
||||
"publicId": "RPT832-00406",
|
||||
"requestNo": "BL-RPT832-000406",
|
||||
"sections": []
|
||||
}
|
||||
```
|
||||
|
||||
### فیلدهای سطح بالا
|
||||
|
||||
#### `title`
|
||||
عنوان کلی گزارش.
|
||||
|
||||
#### `publicId`
|
||||
شناسه عمومی پرونده.
|
||||
|
||||
#### `requestNo`
|
||||
شماره درخواست، اگر در دادههای پرونده موجود باشد.
|
||||
|
||||
#### `sections`
|
||||
آرایهای از سکشنهای گزارش.
|
||||
هر سکشن یک عنوان دارد و شامل تعدادی ردیف اطلاعات (`fields`) است.
|
||||
|
||||
---
|
||||
|
||||
## ساختار هر سکشن
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "زمانبندی پرونده",
|
||||
"fields": [
|
||||
{
|
||||
"label": "تاریخ و ساعت ثبت پرونده",
|
||||
"value": "1405/06/02 07:02"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `title`
|
||||
عنوان فارسی سکشن، آمادهی نمایش در PDF.
|
||||
|
||||
### `fields`
|
||||
لیست ردیفهای اطلاعاتی همان سکشن.
|
||||
|
||||
---
|
||||
|
||||
## ساختار هر فیلد
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "شماره بیمهنامه",
|
||||
"value": "POL-12345"
|
||||
}
|
||||
```
|
||||
|
||||
### `label`
|
||||
عنوان فارسی فیلد.
|
||||
|
||||
### `value`
|
||||
مقدار فیلد.
|
||||
برای نمایش مستقیم در PDF استفاده میشود.
|
||||
|
||||
---
|
||||
|
||||
## سکشنهای ممکن در پاسخ
|
||||
سکشنها معمولاً با ترتیب زیر برمیگردند، ولی فرانتاند بهتر است بهجای تکیه بر ایندکس آرایه، سکشن را با `title` پیدا کند:
|
||||
|
||||
1. `زمانبندی پرونده`
|
||||
2. `مالک خودروی زیان دیده`
|
||||
3. `مالک خودروی مقصر`
|
||||
4. `راننده خودروی زیان دیده`
|
||||
5. `بیمه شخص ثالث زیاندیده`
|
||||
6. `بیمه بدنه زیاندیده`
|
||||
7. `بیمه شخص ثالث مقصر`
|
||||
8. `بیمه بدنه مقصر`
|
||||
9. `اطلاعات خودروی زیاندیده`
|
||||
10. `اطلاعات خودروی مقصر`
|
||||
11. `اظهارات و اقرار زیاندیده`
|
||||
12. `اظهارات و اقرار مقصر`
|
||||
13. `کدهای فناوران`
|
||||
14. `نتیجه ارزیابی`
|
||||
15. `گزارش حادثه`
|
||||
|
||||
نکته:
|
||||
- بعضی سکشنها بسته به نوع پرونده ممکن است وجود نداشته باشند.
|
||||
- در پروندههای بدنه (`CAR_BODY`) اگر طرفین عملاً یک نفر باشند، سکشنهای مربوط به مقصر ممکن است حذف شوند.
|
||||
- در پروندههای شخص ثالث (`THIRD_PARTY`) انتظار میرود اطلاعات هر دو طرف بهصورت تفکیکشده برگردد.
|
||||
|
||||
---
|
||||
|
||||
# توضیح سکشنها
|
||||
|
||||
## 1) `زمانبندی پرونده`
|
||||
برای نمایش زمانهای مهم پرونده.
|
||||
|
||||
فیلدهای مهم:
|
||||
- `تاریخ و ساعت ثبت پرونده`
|
||||
- `تاریخ و ساعت ثبت نتیجه ارزیابی`
|
||||
|
||||
نکته:
|
||||
- این تاریخها در بکاند فرمت شدهاند و آمادهی نمایش هستند.
|
||||
|
||||
---
|
||||
|
||||
## 2) `مالک خودروی زیان دیده`
|
||||
اطلاعات مالک یا صاحب خودروی زیاندیده.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `نام`
|
||||
- `شماره تلفن`
|
||||
- `کد ملی`
|
||||
- `تاریخ تولد`
|
||||
- `شماره شبا`
|
||||
|
||||
نکته:
|
||||
- `شماره شبا` معمولاً برای زیاندیده مهم است و ممکن است فقط در همین سکشن وجود داشته باشد.
|
||||
|
||||
---
|
||||
|
||||
## 3) `مالک خودروی مقصر`
|
||||
اطلاعات مالک خودروی مقصر.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `نام`
|
||||
- `شماره تلفن`
|
||||
- `کد ملی`
|
||||
- `تاریخ تولد`
|
||||
|
||||
نکته:
|
||||
- این سکشن مخصوص پروندههای شخص ثالث اهمیت دارد تا اطلاعات مالک هر دو طرف در PDF موجود باشد.
|
||||
|
||||
---
|
||||
|
||||
## 4) `راننده خودروی زیان دیده`
|
||||
اگر راننده با مالک/بیمهگذار متفاوت باشد، این سکشن برمیگردد.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `نام`
|
||||
- `نوع گواهینامه`
|
||||
- `تاریخ گواهینامه`
|
||||
- `شماره تلفن`
|
||||
- `کد ملی`
|
||||
- `تاریخ تولد`
|
||||
- `شماره گواهینامه`
|
||||
|
||||
نکته:
|
||||
- اگر راننده و مالک یکی باشند، این سکشن ممکن است وجود نداشته باشد.
|
||||
|
||||
---
|
||||
|
||||
## 5) `بیمه شخص ثالث زیاندیده`
|
||||
اطلاعات بیمه شخص ثالث طرف زیاندیده.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `شماره بیمهنامه`
|
||||
- `شرکت بیمه`
|
||||
- `تاریخ شروع بیمهنامه`
|
||||
- `تاریخ پایان بیمهنامه`
|
||||
- `سقف تعهد مالی`
|
||||
- `پوششها`
|
||||
|
||||
---
|
||||
|
||||
## 6) `بیمه بدنه زیاندیده`
|
||||
اطلاعات بیمه بدنهی طرف زیاندیده.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `شماره بیمهنامه`
|
||||
- `شرکت بیمه`
|
||||
- `تاریخ شروع بیمهنامه`
|
||||
- `تاریخ پایان بیمهنامه`
|
||||
- `پوششها`
|
||||
|
||||
---
|
||||
|
||||
## 7) `بیمه شخص ثالث مقصر`
|
||||
اطلاعات بیمه شخص ثالث طرف مقصر.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `شماره بیمهنامه`
|
||||
- `شرکت بیمه`
|
||||
- `تاریخ شروع بیمهنامه`
|
||||
- `تاریخ پایان بیمهنامه`
|
||||
- `سقف تعهد مالی`
|
||||
- `پوششها`
|
||||
|
||||
---
|
||||
|
||||
## 8) `بیمه بدنه مقصر`
|
||||
اطلاعات بیمه بدنهی طرف مقصر.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `شماره بیمهنامه`
|
||||
- `شرکت بیمه`
|
||||
- `تاریخ شروع بیمهنامه`
|
||||
- `تاریخ پایان بیمهنامه`
|
||||
- `پوششها`
|
||||
|
||||
---
|
||||
|
||||
## 9) `اطلاعات خودروی زیاندیده`
|
||||
جزئیات خودروی زیاندیده.
|
||||
|
||||
فیلدها میتوانند شامل موارد زیر باشند:
|
||||
- `خودرو / پلاک`
|
||||
- `خودرو / نام خودرو`
|
||||
- `خودرو / مدل خودرو`
|
||||
- `خودرو / نوع خودرو`
|
||||
- `VIN`
|
||||
- `شماره موتور`
|
||||
- `شماره شاسی`
|
||||
- `رنگ اصلی`
|
||||
- `رنگ فرعی`
|
||||
- `سیستم`
|
||||
- `تیپ`
|
||||
- `کاربری`
|
||||
- `ظرفیت`
|
||||
- `تعداد سیلندر`
|
||||
|
||||
نکته:
|
||||
- بسته به منبع داده، ممکن است بعضی فیلدها با برچسبهای نزدیک به هم ولی از دو منبع مختلف برگردند.
|
||||
|
||||
---
|
||||
|
||||
## 10) `اطلاعات خودروی مقصر`
|
||||
جزئیات خودروی طرف مقصر.
|
||||
|
||||
فیلدها مشابه سکشن خودروی زیاندیده هستند.
|
||||
|
||||
---
|
||||
|
||||
## 11) `اظهارات و اقرار زیاندیده`
|
||||
اطلاعات مربوط به اظهارات طرف زیاندیده.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `نقش طرف`
|
||||
- `نام`
|
||||
- `ادعای خسارت`
|
||||
- `پذیرش نظر کارشناس`
|
||||
- `توضیحات طرف`
|
||||
|
||||
نکته:
|
||||
- معمولاً `اقرار به تقصیر` برای زیاندیده نمایش داده نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## 12) `اظهارات و اقرار مقصر`
|
||||
اطلاعات مربوط به اظهارات طرف مقصر.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `نقش طرف`
|
||||
- `نام`
|
||||
- `اقرار به تقصیر`
|
||||
- `پذیرش نظر کارشناس`
|
||||
- `توضیحات طرف`
|
||||
|
||||
نکته:
|
||||
- معمولاً `ادعای خسارت` برای مقصر نمایش داده نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## 13) `کدهای فناوران`
|
||||
کدها و شناسههای فنی مرتبط با پرونده در فناوران.
|
||||
|
||||
فیلدهای ممکن:
|
||||
- `شماره پرونده فناوران`
|
||||
- `کد پرونده فناوران`
|
||||
- `کد کیس خسارت فناوران`
|
||||
- `کد کارشناسی فناوران`
|
||||
- `کد بیمهنامه فناوران`
|
||||
- `کد راننده فناوران`
|
||||
- `کد نوع خودرو فناوران`
|
||||
- `کد شرکت بیمه فناوران`
|
||||
|
||||
---
|
||||
|
||||
## 14) `نتیجه ارزیابی`
|
||||
اطلاعات نتیجهی ارزیابی کارشناس خسارت.
|
||||
|
||||
فیلدهای مهم:
|
||||
- `نتیجه ارزیابی`
|
||||
- `کارشناس ارزیاب`
|
||||
- `تاریخ و ساعت ثبت ارزیابی`
|
||||
- `پاسخ / توضیحات کارشناس`
|
||||
|
||||
---
|
||||
|
||||
## 15) `گزارش حادثه`
|
||||
خلاصهی اطلاعات حادثه، وضعیت پرونده و برخی خروجیهای کارشناسی.
|
||||
|
||||
فیلدهای رایج:
|
||||
- `تاریخ حادثه`
|
||||
- `ساعت حادثه`
|
||||
- `کارشناس(ان)`
|
||||
- `موقعیت (عرض و طول جغرافیایی)`
|
||||
- `وضعیت آب و هوا`
|
||||
- `وضعیت جاده`
|
||||
- `وضعیت نور`
|
||||
- `وضعیت مقصر`
|
||||
- `وضعیت خسارت`
|
||||
- `نظر کارشناس مقصر`
|
||||
- `نحوه برخورد`
|
||||
- `علت حادثه`
|
||||
- `نوع حادثه`
|
||||
- `توضیحات طرف`
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم برای فرانتاند
|
||||
|
||||
### 1) فقط بر اساس `sections` و `fields` رندر کنید
|
||||
ساختار اصلی خروجی این است:
|
||||
|
||||
```ts
|
||||
response.sections[].title
|
||||
response.sections[].fields[].label
|
||||
response.sections[].fields[].value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2) به ایندکس سکشنها وابسته نشوید
|
||||
ممکن است بعضی سکشنها در بعضی پروندهها وجود نداشته باشند.
|
||||
بهتر است سکشن را با `title` پیدا کنید.
|
||||
|
||||
---
|
||||
|
||||
### 3) نبودن بعضی سکشنها طبیعی است
|
||||
مثلاً:
|
||||
- `راننده خودروی زیان دیده`
|
||||
- سکشنهای مربوط به مقصر در بعضی پروندههای بدنه
|
||||
- بعضی دادههای فناوران
|
||||
|
||||
---
|
||||
|
||||
### 4) مقدار `-` یعنی دادهای برای نمایش وجود نداشته
|
||||
اگر سکشنی دادهی واقعی نداشته باشد، ممکن است فقط این مقدار را داشته باشد:
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "اطلاعات",
|
||||
"value": "-"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5) برچسبها فارسی و آمادهی نمایش هستند
|
||||
فیلدهای `title` و `label` نیازی به ترجمهی مجدد در فرانتاند ندارند.
|
||||
|
||||
---
|
||||
|
||||
## نمونهی سادهی رندر در فرانتاند
|
||||
|
||||
```ts
|
||||
for (const section of response.sections) {
|
||||
renderSectionTitle(section.title)
|
||||
|
||||
for (const field of section.fields) {
|
||||
renderRow(field.label, field.value ?? "-")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## خلاصه
|
||||
این API برای تولید PDF پرونده، دادهها را بهصورت کامل و تفکیکشده برمیگرداند، از جمله:
|
||||
|
||||
- زمانبندی پرونده
|
||||
- اطلاعات مالک زیاندیده
|
||||
- اطلاعات مالک مقصر
|
||||
- اطلاعات راننده در صورت متفاوت بودن
|
||||
- بیمهنامههای تفکیکشدهی ثالث و بدنه برای هر طرف
|
||||
- اطلاعات خودرو برای هر دو طرف
|
||||
- اظهارات و اقرار هر دو طرف
|
||||
- کدهای فناوران
|
||||
- نتیجه ارزیابی و توضیحات کارشناس
|
||||
- گزارش حادثه
|
||||
1
err.json
1
err.json
@@ -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":""}}
|
||||
@@ -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
18169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
104
package.json
104
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
91
scripts/data/fanavaran-flow.env.example
Normal file
91
scripts/data/fanavaran-flow.env.example
Normal 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=
|
||||
65
scripts/data/parsian-tehran/branches.json
Normal file
65
scripts/data/parsian-tehran/branches.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
153
scripts/data/parsian-tehran/field-experts.json
Normal file
153
scripts/data/parsian-tehran/field-experts.json
Normal 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": "كارشناس ارزياب خسارت ثالث مالي"
|
||||
}
|
||||
]
|
||||
}
|
||||
464
scripts/data/parsian-tehran/file-makers.json
Normal file
464
scripts/data/parsian-tehran/file-makers.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
161
scripts/data/parsian-tehran/file-reviewers.json
Normal file
161
scripts/data/parsian-tehran/file-reviewers.json
Normal 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
143
scripts/fanavaran-auth.sh
Executable 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
1111
scripts/fanavaran-flow-test.sh
Executable file
File diff suppressed because it is too large
Load Diff
920
scripts/refresh-blame-inquiries.js
Normal file
920
scripts/refresh-blame-inquiries.js
Normal 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));
|
||||
}
|
||||
2487
scripts/seed-insurer-reports-fixtures.ts
Normal file
2487
scripts/seed-insurer-reports-fixtures.ts
Normal file
File diff suppressed because it is too large
Load Diff
512
scripts/seed-parsian-tehran.ts
Normal file
512
scripts/seed-parsian-tehran.ts
Normal 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);
|
||||
});
|
||||
86
scripts/verify-plate-normalization.mjs
Normal file
86
scripts/verify-plate-normalization.mjs
Normal 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);
|
||||
@@ -3,6 +3,9 @@ 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",
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
//! NEW
|
||||
export enum CaseStatus {
|
||||
|
||||
OPEN = "OPEN",
|
||||
OPEN = "OPEN",
|
||||
|
||||
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
|
||||
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
|
||||
|
||||
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
|
||||
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
|
||||
|
||||
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
||||
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
||||
|
||||
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||
|
||||
COMPLETED = "COMPLETED",
|
||||
/**
|
||||
* 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",
|
||||
|
||||
CANCELLED = "CANCELLED",
|
||||
/**
|
||||
* 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",
|
||||
|
||||
AUTO_CLOSED = "AUTO_CLOSED"
|
||||
}
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
97
src/Types&Enums/blame-request-management/resend-item-ui.ts
Normal file
97
src/Types&Enums/blame-request-management/resend-item-ui.ts
Normal 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];
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -11,11 +11,50 @@ 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",
|
||||
|
||||
@@ -7,19 +7,27 @@ export enum ClaimWorkflowStep {
|
||||
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",
|
||||
|
||||
|
||||
@@ -3,4 +3,6 @@ export enum InPersonDocumentsEnum {
|
||||
CarCertificate = "carCertificate",
|
||||
DrivingLicense = "drivingLicense",
|
||||
CarGreenCard = "carGreenCard",
|
||||
Plate = "plate",
|
||||
CarPlate = "carPlate",
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum TypeOfDamage {
|
||||
Repair = "repair",
|
||||
Change = "change",
|
||||
Repair = "تعمیر",
|
||||
Change = "تعویض",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
@@ -140,100 +107,69 @@ export class AiService implements OnModuleInit {
|
||||
? 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";
|
||||
@@ -242,7 +178,10 @@ export class AiService implements OnModuleInit {
|
||||
|
||||
if (er.response) {
|
||||
errorSource = "AI_SERVICE_RESPONSE";
|
||||
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`;
|
||||
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}`;
|
||||
@@ -255,14 +194,6 @@ export class AiService implements OnModuleInit {
|
||||
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}`,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
src/auth/auth-services/captcha-auth.error.ts
Normal file
38
src/auth/auth-services/captcha-auth.error.ts
Normal 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));
|
||||
}
|
||||
46
src/auth/auth-services/user-auth-error.ts
Normal file
46
src/auth/auth-services/user-auth-error.ts
Normal 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));
|
||||
}
|
||||
218
src/auth/auth-services/user-link-access.service.ts
Normal file
218
src/auth/auth-services/user-link-access.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
18
src/auth/dto/actor/create-registrar.actor.dto.ts
Normal file
18
src/auth/dto/actor/create-registrar.actor.dto.ts
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
28
src/auth/dto/captcha-response.dto.ts
Normal file
28
src/auth/dto/captcha-response.dto.ts
Normal 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;
|
||||
}
|
||||
15
src/auth/dto/get-captcha-image-query.dto.ts
Normal file
15
src/auth/dto/get-captcha-image-query.dto.ts
Normal 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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
41
src/auth/guards/settings-jwt.guard.ts
Normal file
41
src/auth/guards/settings-jwt.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
112
src/captcha/captcha-challenge.service.ts
Normal file
112
src/captcha/captcha-challenge.service.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
28
src/captcha/captcha.module.ts
Normal file
28
src/captcha/captcha.module.ts
Normal 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 {}
|
||||
43
src/captcha/captcha.service.ts
Normal file
43
src/captcha/captcha.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/captcha/entities/schema/captcha-challenge.schema.ts
Normal file
32
src/captcha/entities/schema/captcha-challenge.schema.ts
Normal 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);
|
||||
465
src/case-expert-report/case-expert-report.builder.spec.ts
Normal file
465
src/case-expert-report/case-expert-report.builder.spec.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
import { buildInsurerFileReport } from "./case-expert-report.builder";
|
||||
import { PR } from "./persian-report-labels";
|
||||
|
||||
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 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",
|
||||
participants: [
|
||||
{
|
||||
participantId: "DRIVER",
|
||||
fullName: "راننده نمونه",
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
licenseNumber: "LIC-1",
|
||||
},
|
||||
{
|
||||
participantId: "VEHICLE_OWNER",
|
||||
fullName: "زیان دیده نمونه",
|
||||
nationalCode: "1234567890",
|
||||
birthday: "1370/01/01",
|
||||
},
|
||||
],
|
||||
participantRoles: {
|
||||
driver: "DRIVER",
|
||||
vehicleOwner: "VEHICLE_OWNER",
|
||||
thirdPartyPolicyholder: "VEHICLE_OWNER",
|
||||
},
|
||||
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: "کارشناس خسارت نمونه" },
|
||||
parts: [
|
||||
{
|
||||
partId: 12,
|
||||
carPartDamage: { label_fa: "سپر جلو" },
|
||||
typeOfDamage: "تعویض",
|
||||
price: 2_500_000,
|
||||
salary: 500_000,
|
||||
totalPayment: 3_000_000,
|
||||
factorNeeded: false,
|
||||
daghi: { option: "تحویل داغی" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFieldValue(report, PR.ownerSection, PR.name)).toBe(
|
||||
"زیان دیده نمونه",
|
||||
);
|
||||
expect(
|
||||
getFieldValue(report, PR.damagedParticipantsSection, PR.driverRole),
|
||||
).toContain("راننده نمونه");
|
||||
expect(
|
||||
getFieldValue(
|
||||
report,
|
||||
PR.damagedParticipantsSection,
|
||||
PR.thirdPartyPolicyholderRole,
|
||||
),
|
||||
).toContain("1234567890");
|
||||
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(
|
||||
"1405/05/19 13:23",
|
||||
);
|
||||
expect(
|
||||
getFieldValue(report, PR.timelineSection, PR.evaluationRegisteredAt),
|
||||
).toBe("1405/05/19 16:00");
|
||||
|
||||
expect(
|
||||
getFieldValue(report, PR.evaluationSection, PR.evaluationResult),
|
||||
).toBe("تأیید شده");
|
||||
expect(
|
||||
getFieldValue(report, PR.evaluationSection, PR.evaluationExpert),
|
||||
).toBe("کارشناس خسارت نمونه");
|
||||
expect(
|
||||
getFieldValue(report, PR.evaluationSection, PR.evaluationResponse),
|
||||
).toBe("نیاز به تعویض سپر جلو");
|
||||
expect(getFieldValue(report, "قیمت قطعات", "قطعه ۱ / نام قطعه")).toBe(
|
||||
"سپر جلو",
|
||||
);
|
||||
expect(getFieldValue(report, "قیمت قطعات", "قطعه ۱ / قیمت قطعه")).toBe(
|
||||
"۲٬۵۰۰٬۰۰۰ تومان",
|
||||
);
|
||||
expect(getFieldValue(report, "قیمت قطعات", "قطعه ۱ / مبلغ کل")).toBe(
|
||||
"۳٬۰۰۰٬۰۰۰ تومان",
|
||||
);
|
||||
});
|
||||
|
||||
it("localizes legacy English car-body environmental conditions", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00012" },
|
||||
blame: {
|
||||
type: "CAR_BODY",
|
||||
parties: [
|
||||
{
|
||||
role: "FIRST",
|
||||
person: { userId: "damaged", fullName: "بیمهگذار" },
|
||||
statement: {
|
||||
weatherCondition: "CLEAR",
|
||||
roadCondition: "wet",
|
||||
lightCondition: "LOW_LIGHT",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFieldValue(report, PR.accidentSection, PR.weather)).toBe("صاف");
|
||||
expect(getFieldValue(report, PR.accidentSection, PR.road)).toBe("مرطوب");
|
||||
expect(getFieldValue(report, PR.accidentSection, PR.light)).toBe("کمنور");
|
||||
});
|
||||
|
||||
it("exposes the damage expert part-price result as a separate section", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00013" },
|
||||
claim: {
|
||||
claimStatus: "APPROVED",
|
||||
evaluation: {
|
||||
damageExpertReply: {
|
||||
description: "تعویض قطعه ضروری است",
|
||||
parts: [
|
||||
{
|
||||
carPartDamage: { label_fa: "چراغ جلو" },
|
||||
price: 4_000_000,
|
||||
totalPayment: 4_000_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFieldValue(report, "قیمت قطعات", "قطعه ۱ / نام قطعه")).toBe(
|
||||
"چراغ جلو",
|
||||
);
|
||||
expect(getFieldValue(report, "قیمت قطعات", "قطعه ۱ / قیمت قطعه")).toBe(
|
||||
"۴٬۰۰۰٬۰۰۰ تومان",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to claim history for the evaluation submission time", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00014" },
|
||||
claim: {
|
||||
evaluation: {
|
||||
damageExpertReply: { description: "ارزیابی ثبت شد" },
|
||||
},
|
||||
history: [
|
||||
{
|
||||
type: "EXPERT_REPLY_SUBMITTED",
|
||||
timestamp: "2026-08-10T12:30:45.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getFieldValue(report, PR.timelineSection, PR.evaluationRegisteredAt),
|
||||
).toBe("1405/05/19 16:00");
|
||||
expect(
|
||||
getFieldValue(report, PR.evaluationSection, PR.evaluationSubmittedAt),
|
||||
).toBe("1405/05/19 16:00");
|
||||
});
|
||||
|
||||
it("localizes every boolean value", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00015" },
|
||||
blame: {
|
||||
type: "CAR_BODY",
|
||||
parties: [
|
||||
{
|
||||
role: "FIRST",
|
||||
person: { userId: "damaged", fullName: "بیمهگذار" },
|
||||
vehicle: {
|
||||
isNew: true,
|
||||
hasEndorsement: false,
|
||||
source: "ESG_CAR_BODY_INQUIRY",
|
||||
inquiry: { source: "TEJARAT_BLOCK_INQUIRY" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fields = report.sections.flatMap((section) => section.fields);
|
||||
expect(
|
||||
fields.find((field) => field.label === "خودرو / خودرو نو")?.value,
|
||||
).toBe("بله");
|
||||
expect(fields.some((field) => field.value === "خیر")).toBe(true);
|
||||
expect(fields.some((field) => field.value === "true")).toBe(false);
|
||||
expect(fields.some((field) => field.value === "false")).toBe(false);
|
||||
});
|
||||
|
||||
it("hides ESG and TEJARAT inquiry source details", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00016" },
|
||||
blame: {
|
||||
type: "CAR_BODY",
|
||||
parties: [
|
||||
{
|
||||
role: "FIRST",
|
||||
person: { userId: "damaged", fullName: "بیمهگذار" },
|
||||
vehicle: {
|
||||
source: "ESG_CAR_BODY_INQUIRY",
|
||||
inquiry: { source: "TEJARAT_BLOCK_INQUIRY" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fields = report.sections.flatMap((section) => section.fields);
|
||||
expect(fields.some((field) => String(field.value).includes("ESG"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
fields.some((field) => String(field.value).includes("TEJARAT")),
|
||||
).toBe(false);
|
||||
expect(fields.some((field) => field.label.includes("منبع"))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses the stored V4 licence type for a driver who differs from the policyholder", () => {
|
||||
const report = buildInsurerFileReport({
|
||||
overview: { publicId: "A00011" },
|
||||
blame: {
|
||||
type: "THIRD_PARTY",
|
||||
parties: [
|
||||
{
|
||||
role: "FIRST",
|
||||
person: { userId: "guilty", fullName: "مقصر" },
|
||||
},
|
||||
{
|
||||
role: "SECOND",
|
||||
person: {
|
||||
userId: "damaged",
|
||||
fullName: "بیمهگذار زیاندیده",
|
||||
driverIsInsurer: false,
|
||||
nationalCodeOfDriver: "0123456789",
|
||||
driverLicense: "L-123",
|
||||
licenseType: "پایه یک",
|
||||
},
|
||||
},
|
||||
],
|
||||
expert: { decision: { guiltyPartyId: "guilty" } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFieldValue(report, PR.driverSection, PR.nationalCode)).toBe(
|
||||
"0123456789",
|
||||
);
|
||||
expect(getFieldValue(report, PR.driverSection, PR.licenseType)).toBe(
|
||||
"پایه یک",
|
||||
);
|
||||
});
|
||||
});
|
||||
1151
src/case-expert-report/case-expert-report.builder.ts
Normal file
1151
src/case-expert-report/case-expert-report.builder.ts
Normal file
File diff suppressed because it is too large
Load Diff
61
src/case-expert-report/case-expert-report.controller.ts
Normal file
61
src/case-expert-report/case-expert-report.controller.ts
Normal 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/case-expert-report/case-expert-report.module.ts
Normal file
12
src/case-expert-report/case-expert-report.module.ts
Normal 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 {}
|
||||
27
src/case-expert-report/case-expert-report.service.ts
Normal file
27
src/case-expert-report/case-expert-report.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/case-expert-report/case-expert-report.types.ts
Normal file
16
src/case-expert-report/case-expert-report.types.ts
Normal 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[];
|
||||
};
|
||||
290
src/case-expert-report/persian-report-labels.ts
Normal file
290
src/case-expert-report/persian-report-labels.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
export const PR = {
|
||||
reportTitle: "گزارش پرونده بیمه گر",
|
||||
publicId: "شناسه عمومی",
|
||||
requestNo: "شماره درخواست",
|
||||
empty: "-",
|
||||
ownerSection: "مالک خودروی زیان دیده",
|
||||
guiltyOwnerSection: "مالک خودروی مقصر",
|
||||
damagedParticipantsSection: "اشخاص و نقشهای خودروی زیاندیده",
|
||||
guiltyParticipantsSection: "اشخاص و نقشهای خودروی مقصر",
|
||||
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: "پاسخ / توضیحات کارشناس",
|
||||
evaluationPartsSection: "قیمت قطعات",
|
||||
part: "قطعه",
|
||||
partName: "نام قطعه",
|
||||
damageType: "نوع خسارت",
|
||||
partPrice: "قیمت قطعه",
|
||||
repairSalary: "اجرت تعمیر",
|
||||
totalPayment: "مبلغ کل",
|
||||
factorNeeded: "نیازمند فاکتور",
|
||||
daghi: "داغی",
|
||||
admitsGuilt: "اقرار به تقصیر",
|
||||
claimsDamage: "ادعای خسارت",
|
||||
acceptsExpertOpinion: "پذیرش نظر کارشناس",
|
||||
partyRole: "نقش طرف",
|
||||
driverRole: "راننده",
|
||||
vehicleOwnerRole: "مالک وسیله نقلیه",
|
||||
thirdPartyPolicyholderRole: "بیمهگذار شخص ثالث",
|
||||
carBodyPolicyholderRole: "بیمهگذار بدنه",
|
||||
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: "خیر",
|
||||
};
|
||||
|
||||
const WEATHER_LABELS: Record<string, string> = {
|
||||
clear: "صاف",
|
||||
sunny: "صاف",
|
||||
rainy: "بارانی",
|
||||
rain: "بارانی",
|
||||
snowy: "برفی",
|
||||
snow: "برفی",
|
||||
foggy: "مهآلود",
|
||||
fog: "مهآلود",
|
||||
};
|
||||
|
||||
const ROAD_LABELS: Record<string, string> = {
|
||||
muddy: "گلی",
|
||||
icy: "یخزده",
|
||||
wet: "مرطوب",
|
||||
dry: "خشک",
|
||||
};
|
||||
|
||||
const LIGHT_LABELS: Record<string, string> = {
|
||||
daylight: "روز",
|
||||
day: "روز",
|
||||
night: "شب",
|
||||
low_light: "کمنور",
|
||||
lowlight: "کمنور",
|
||||
};
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export function persianAccidentCondition(
|
||||
kind: "weather" | "road" | "light",
|
||||
value: unknown,
|
||||
): string | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
const raw = String(value).trim();
|
||||
const normalized = raw.toLowerCase().replace(/[\s-]+/g, "_");
|
||||
const labels =
|
||||
kind === "weather"
|
||||
? WEATHER_LABELS
|
||||
: kind === "road"
|
||||
? ROAD_LABELS
|
||||
: LIGHT_LABELS;
|
||||
return labels[normalized] ?? raw;
|
||||
}
|
||||
59
src/claim-request-management/claim-details-access.v2.spec.ts
Normal file
59
src/claim-request-management/claim-details-access.v2.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -22,24 +22,26 @@ 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";
|
||||
import { UserCommentDto } from "./dto/user-comment.dto";
|
||||
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 +50,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 +64,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 +80,7 @@ export class ClaimRequestManagementController {
|
||||
);
|
||||
}
|
||||
|
||||
// @ApiOperation({ deprecated: true })
|
||||
@Get("/car-other-part")
|
||||
async getCarOtherParts() {
|
||||
const carOtherPart = await readFile(
|
||||
@@ -85,12 +90,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 +109,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 +125,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 +170,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 +196,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 +224,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 +247,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 +269,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 +283,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 +315,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 +353,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 +383,7 @@ export class ClaimRequestManagementController {
|
||||
callback(null, filename);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
}),
|
||||
)
|
||||
async uploadDocuments(
|
||||
@@ -393,9 +404,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 +420,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 +446,7 @@ export class ClaimRequestManagementController {
|
||||
callback(null, filename);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
}),
|
||||
)
|
||||
async uploadFactorForPart(
|
||||
@@ -450,39 +463,40 @@ export class ClaimRequestManagementController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBody({ type: InPersonVisitDto })
|
||||
@ApiParam({ name: "id" })
|
||||
// @ApiParam({ name: "id" })
|
||||
// @ApiOperation({ deprecated: true })
|
||||
@Patch(":id/visit")
|
||||
async inPersonVisit(
|
||||
@Param("id") requestId: string,
|
||||
@Body() body: InPersonVisitDto,
|
||||
@CurrentUser() actor,
|
||||
) {
|
||||
// Pass the branchId from the body to the service
|
||||
return await this.claimRequestManagementService.inPersonVisit(
|
||||
requestId,
|
||||
body.branchId,
|
||||
actor,
|
||||
);
|
||||
return await this.claimRequestManagementService.inPersonVisit(requestId, actor);
|
||||
}
|
||||
|
||||
@Get("branches/:insuranceId")
|
||||
async insuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||
return await this.claimRequestManagementService.retrieveInsuranceBranches(
|
||||
insuranceId,
|
||||
);
|
||||
}
|
||||
// Legacy V1 owner branch picker — intentionally disabled.
|
||||
// A claim is handled by an expert assigned to the insurer branch, so the
|
||||
// owner must not choose an insurer branch while signing. This does not apply
|
||||
// to the separate expert-only `daghi.branchId` choice in pricing flows.
|
||||
//
|
||||
// @Get("branches/:insuranceId")
|
||||
// async insuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||
// 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,
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user