fix fish variz name, dalilrad, sms, bale
This commit is contained in:
1 parent
90931a5a64
commit
35a1ec1794
30 files changed
+674
-39
No files matched your search
+160
-9
@@ -1,11 +1,11 @@
|
||||
Project Root: customerTicket
|
||||
Generated At: 2026-10-07T07:30:58+00:00
|
||||
Python Files: 287
|
||||
Generated At: 2026-10-11T08:57:27+00:00
|
||||
Python Files: 304
|
||||
Django Files: 49
|
||||
JavaScript/TypeScript Files: 38
|
||||
HTML Files: 72
|
||||
Total Files: 1264
|
||||
Total Directories: 131
|
||||
JavaScript/TypeScript Files: 39
|
||||
HTML Files: 73
|
||||
Total Files: 1412
|
||||
Total Directories: 134
|
||||
|
||||
customerTicket
|
||||
├── .agents
|
||||
@@ -158,10 +158,13 @@ customerTicket
|
||||
│ │ │ ├── 0003_paymentnotice_issuance_key.py
|
||||
│ │ │ ├── 0004_paymentaccesslink_paymentnotification_access_link_and_more.py
|
||||
│ │ │ ├── 0005_paymentnotice_deleted_at.py
|
||||
│ │ │ ├── 0006_normalize_deposit_identifiers.py
|
||||
│ │ │ ├── 0007_receipt_alerts.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── access.py
|
||||
│ │ ├── admin.py
|
||||
│ │ ├── alert_templates.py
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── context_processors.py
|
||||
│ │ ├── customer_picker.py
|
||||
@@ -172,6 +175,7 @@ customerTicket
|
||||
│ │ ├── models.py
|
||||
│ │ ├── notifications.py
|
||||
│ │ ├── policies.py
|
||||
│ │ ├── receipt_alerts.py
|
||||
│ │ ├── selectors.py
|
||||
│ │ ├── services.py
|
||||
│ │ ├── staff_urls.py
|
||||
@@ -303,6 +307,7 @@ customerTicket
|
||||
│ ├── ORGANIZATIONAL_TICKETING_GAP_ANALYSIS.md
|
||||
│ ├── PAYMENT_LINKS.md
|
||||
│ ├── PAYMENT_NOTICE_DELETION.md
|
||||
│ ├── PAYMENT_RECEIPT_ALERTS.md
|
||||
│ ├── PHONE_CONTACTS_API.md
|
||||
│ ├── PRODUCTION_SEED.md
|
||||
│ ├── QA_PERMISSION_MATRIX.md
|
||||
@@ -376,6 +381,13 @@ customerTicket
|
||||
│ │ ├── graph.json
|
||||
│ │ ├── GRAPH_REPORT.md
|
||||
│ │ └── manifest.json
|
||||
│ ├── 2026-10-07
|
||||
│ │ ├── .graphify_analysis.json
|
||||
│ │ ├── .graphify_labels.json
|
||||
│ │ ├── cost.json
|
||||
│ │ ├── graph.json
|
||||
│ │ ├── GRAPH_REPORT.md
|
||||
│ │ └── manifest.json
|
||||
│ ├── before-html-semantic-2026-10-04T06-37-15-050Z
|
||||
│ │ ├── .graphify_analysis.json
|
||||
│ │ ├── .graphify_labels.json
|
||||
@@ -390,20 +402,25 @@ customerTicket
|
||||
│ │ │ ├── 00aea1c179c20548df2499948796faba373334997bfeddad9e335db138753cc5.json
|
||||
│ │ │ ├── 00afff188288984e240329931ac9e4273f5ab89be582e021e9edbdbead42ebfe.json
|
||||
│ │ │ ├── 0138bfeaa54b14916df0028a970de356913b83e8da2cd5cfc610361887baa9c2.json
|
||||
│ │ │ ├── 01b7d6279ec1e552558a7073054667e5d6a65ba01ccd9ed56ff5b916f50c3866.json
|
||||
│ │ │ ├── 01d84090a3b22716edb63a5cf9c0a56cc94c0f5207cdbd5005843f858bbb292f.json
|
||||
│ │ │ ├── 024a4481dc8a483812552f07ba72ef1e8cc3569ffde87b00603f55c8d1f47fae.json
|
||||
│ │ │ ├── 02a3519f5b471b7083c161d9d9c73ab5b0ee78cf7e2cc4d5be7c2542435aa58e.json
|
||||
│ │ │ ├── 03040dc947a4bba22aafbc6262c9db2c8549826ae5aa1ac4cc1821806f93561f.json
|
||||
│ │ │ ├── 039590a02c184d21a28caec4178b981a824ec4356a225d1ce546cc212d2f550f.json
|
||||
│ │ │ ├── 040cdac326eb1631b97e9aeddca78ac07e236243b5b8ab2d77bac37b50a91cdd.json
|
||||
│ │ │ ├── 04b84d19d132764512441a31b31294e4f19dc963ed0a6e3abdd3eca7719e8bca.json
|
||||
│ │ │ ├── 057ccb6901a83922bee4265993977ceb0d0ac425c900cb2d2f3181998df71796.json
|
||||
│ │ │ ├── 058b4e36240cd25d158647c300472baad5e7101ebde0319883abb0edc8c9c74d.json
|
||||
│ │ │ ├── 06ad1912a277e46fe5cecaa8a59b370c18101329c055c26fa6d603f37f63c91a.json
|
||||
│ │ │ ├── 06c990ea0019d34b73df76d59c36bc829e2551dfbf021134ea40320c3c602a09.json
|
||||
│ │ │ ├── 07e174cf218887aa0f8747199874faf6bc8af9347e43ff9b5a2073c089637f6f.json
|
||||
│ │ │ ├── 08ff4daae812563107028890c2ae7257bb5f9a376a978244c814b7a4c1d10a98.json
|
||||
│ │ │ ├── 091194a653ec9ff59e50d6d6121d12c0b0238b3f5cbb0467133a224b7eda59c8.json
|
||||
│ │ │ ├── 09f9369eaae9b6144d8967b80f1a3edf0b3a8a3c44ef7e7f453b48b0adaa3fac.json
|
||||
│ │ │ ├── 0aa59f78fa092e41298775a4db40136ea63654d45e3ae835903c4e3dbd0578d4.json
|
||||
│ │ │ ├── 0aafac1c4b289aeef9a6251e3a12fbf1710ae576dbd647d155aa3981008d50b7.json
|
||||
│ │ │ ├── 0b7e468b727d067e58463acadbb75af9f59ab5db4b879c11dfa0463321ac125a.json
|
||||
│ │ │ ├── 0c2c38fa53a0fe1da084703272dff1c8130732eb327a788d26ec0443c72d6b96.json
|
||||
│ │ │ ├── 0d276b031b1e68ce07a7e0620645994c4d80c88dbdeb4b49deba5e053ef188cb.json
|
||||
│ │ │ ├── 0d5f84af41009fdecae2a46d4ad5fab38a79c1a5dde262e9c7f8d35707a35092.json
|
||||
@@ -411,6 +428,7 @@ customerTicket
|
||||
│ │ │ ├── 0f0c4146a70cadafffd025d6954e9202626be74ff97539e984c8b27cda5ed662.json
|
||||
│ │ │ ├── 0f28b01b2484ce84b5251ff848065a2c6f7f574665305b5ecf8cb31a755a7982.json
|
||||
│ │ │ ├── 0feebff2f8ac6ee66557b8692e3a537c0834b6bc4c28a6b9da6f3edf619283c1.json
|
||||
│ │ │ ├── 118544d17e045bbbae483669adbc365dbc9b0d707514cff54c8b84ca16042967.json
|
||||
│ │ │ ├── 11d2b71feb1480622fb18ee3795cf070421b146e4273b5016fad81e7cc8e2c7e.json
|
||||
│ │ │ ├── 11ff4da3afbce9cae19102862deefdf725fa363130fad753fa189fa4f6c8c4de.json
|
||||
│ │ │ ├── 12a877e28c12d0651d6487751c6b746afbe8361f9ba7e1b672f09bd596fbcf82.json
|
||||
@@ -418,10 +436,13 @@ customerTicket
|
||||
│ │ │ ├── 12f95828a2c1818402459627e36481199020e31faa280d214f81700fb9ad520d.json
|
||||
│ │ │ ├── 1436cce90bf2f00f050c9eb67cddf5672ed6a3db185ef663c257ee7936b175ae.json
|
||||
│ │ │ ├── 143ae363276b4a1db64805529e618db71c204aa56bc8b54761693a9a2060336c.json
|
||||
│ │ │ ├── 14ffc999200b172d3919e3d9899656c1f8a212769b1e86fe388b82fa302ed1be.json
|
||||
│ │ │ ├── 15539048155c760644fc70dd1fbff3855ceedf11bc6ef7b270d1c7280cb4df48.json
|
||||
│ │ │ ├── 175f840f1587093869a030b52c3b019e020c2fb7cf1223579b251b4857752130.json
|
||||
│ │ │ ├── 17ab5521ea572ea85443db030bd5085f1443c20b20da9cbf808f21ad695eb319.json
|
||||
│ │ │ ├── 18ba25713068baf53992eb4ea94f95212ecaeb931fdcf6769970fc2fd19b2f93.json
|
||||
│ │ │ ├── 19ed1276256eb4d44b9cea2dddac4de3801d67a08cf381a55466e82ee6f2bbc6.json
|
||||
│ │ │ ├── 19ee52d79b7d8eec47a4708aff6c85884496a64aaf84591b70505ce03999283c.json
|
||||
│ │ │ ├── 1bfc839bf502b97d0b70b0bacc9781cae7511d27d078ed6ac6978fd438833dd1.json
|
||||
│ │ │ ├── 1c74c48acbde13e6bc5c2f63b204a1c051f22a8f48cd7b40ac54fc95e8809018.json
|
||||
│ │ │ ├── 1c9f81b7412f571aa252ed10230220684b6657e1115f75a8c3a64b792517d41c.json
|
||||
@@ -429,27 +450,38 @@ customerTicket
|
||||
│ │ │ ├── 1d6b17be6b2c0e7d25b4622b4a8c09ab705cecf9db09e2f86c35a4a564485791.json
|
||||
│ │ │ ├── 1db075565e2fe1027675b7ee03d055150e6b741f3e9865bcd46d9a6113c538a8.json
|
||||
│ │ │ ├── 1e3bef21b169a2020643dc56fc33c665acc164b4b8f07639a9b1f58c37bb4caf.json
|
||||
│ │ │ ├── 1e4a0b7beaeed9f0f2d7088d5944743bf6307504aa956e35ed8ce54bede8ac5a.json
|
||||
│ │ │ ├── 1f440edc6feb5bf8c2a462b1b4a0b44bd22b4a763286fb2b340c797c282b5bcd.json
|
||||
│ │ │ ├── 1f6cfd55d13bc48aa6c68558090ceeaf24a54c7bec690d51d95243c904daf0a6.json
|
||||
│ │ │ ├── 1fabd63a56e826ff351d3edca222f5f958004c01f0bd16aed30080e8b13da33f.json
|
||||
│ │ │ ├── 21f38c2f6a379ab402b4d3bd1b0809129c8554ebb01202a160c42a1e9b49b717.json
|
||||
│ │ │ ├── 22534d744aadf9c9b61b908eccea0e9b3eaa30bc381bace54b3221ee0861a0ec.json
|
||||
│ │ │ ├── 22df765bbeaca09caaf2fbcddcdd3ef1d4c0800f0c545f963bd91e85c33fe7ec.json
|
||||
│ │ │ ├── 2316fed463c7d57796582cc906b61aa3e80f0b3614794278f32a644dcff1192e.json
|
||||
│ │ │ ├── 23409e99066ce9ff8052a0031e7359aa9eb35c0abe27cd707616f4e814dd403e.json
|
||||
│ │ │ ├── 2363fafda0db2a268722cdc649cfb520f1d2e7f888debc09528ea8dc4aab0b60.json
|
||||
│ │ │ ├── 2396802d9ca82a055f431939b4cd95eea6c29556956075d1d1ff35e4bca3b7c6.json
|
||||
│ │ │ ├── 244d7b4b6409bf25f038ee2751756e867872e873deb39a2c0319233b208bac01.json
|
||||
│ │ │ ├── 252160e3514d77f06df93fd38cddf31464cc41b33126f89b7cf3732afd80ac0f.json
|
||||
│ │ │ ├── 254ff59f2f43fd16c2028b338ec47a892294021f1a354238db59cb2ae6cbdf4a.json
|
||||
│ │ │ ├── 2609e627752bd20df923f6c69e090173a368757b3dbd2ce418e3cc9f67c697ba.json
|
||||
│ │ │ ├── 26a41d5fda805c545065f2da12215d350587a61aff951937f54d64bb713f0fab.json
|
||||
│ │ │ ├── 26c6832fa65fe38fa62f98bbfd8d544cf6a470cce547579e1d650390e8b9fded.json
|
||||
│ │ │ ├── 26e3b3874f2dc2c3709945e2f6cf050e5c525933de9f09a8c91b244e3bfa8ca7.json
|
||||
│ │ │ ├── 27668af545c6617927904e70fa532e756b5ecdf46e3071256fa379ce3bdc9974.json
|
||||
│ │ │ ├── 2782dd32ec82052ba614fbf227165cf220ac3475521b90ea987f648d080979a5.json
|
||||
│ │ │ ├── 28cefeff27c9cb5760aead796c37afdb1311bff0a5ff759136f0473ffa0152ff.json
|
||||
│ │ │ ├── 298888276da87757f59b0e0280ea46039c0ee05945961c3e00b287abcbc6a2f0.json
|
||||
│ │ │ ├── 29aca949b15e953360acae5ba86aac6c34ba8efddc55636fa05c28bd259ec66e.json
|
||||
│ │ │ ├── 2a68e54ab0e9bec51e402455f9592be0fb5f1dbf18210f8338f01fc35b54fd2e.json
|
||||
│ │ │ ├── 2b7cc5d0dec7234082c1797ab637b916ac7df39277f8fbf109ae90190fc1e900.json
|
||||
│ │ │ ├── 2c76016c5b2c45bddcb0aec92b2365ea389feafb0667b2367350c756c7c51d87.json
|
||||
│ │ │ ├── 2c8d91fbfca457646cf60edb03fd3a89a2494a6b6612087854edeac5a119739a.json
|
||||
│ │ │ ├── 2cedf41a22ec35c4568262c1fbb02208903d5ae3d91d6bee9192012c136208c6.json
|
||||
│ │ │ ├── 2df1edd1fd32c467f237babe4a94f2ed73b67270ede6fa8298b32eab7c3eb80b.json
|
||||
│ │ │ ├── 2e7903ddca9e29c53f1706f2755303e56d60436e494d066d011fdba93e701903.json
|
||||
│ │ │ ├── 2edf2af73b8c2b6f4f195ec878746ca0ecc6575576fec24c2cd5be8d7d30d4a4.json
|
||||
│ │ │ ├── 2f2e2e2f48d6f1c856d699606fbc6705871328d271a8ef3fb7bb9e638449d40f.json
|
||||
│ │ │ ├── 2fd5953e7926f320bdeac2d559967758aef4632bf7dfa26a5ee50ad386fc9b67.json
|
||||
│ │ │ ├── 30a1c433d0992b236232f8d17b7e17bbcfac1fd7385c607d1d09e716ba7da9db.json
|
||||
│ │ │ ├── 3142f9d4517f137483c716dd61985811bbf006b85506791e9a2ee37d58b2f53e.json
|
||||
@@ -461,6 +493,7 @@ customerTicket
|
||||
│ │ │ ├── 345cea9e55c5f1954119121edf5ec28f1514b11d83de6c5bddf80d014bd98c94.json
|
||||
│ │ │ ├── 34b48eae711a65d522f81081262d437469f08182368d46643dc80eadd96bbdb5.json
|
||||
│ │ │ ├── 34cbb71eca44ad5849b04248e513e7cc33c3d3f1a1f66cc27f732666e585bf19.json
|
||||
│ │ │ ├── 35087fe77e282f61d742ba82850f71b99fb5a515c98302e214809a662caace81.json
|
||||
│ │ │ ├── 36000938c9c3b13fbd92622f905f8c4f4bfbfdabe66e3357e88c162a4fb198ce.json
|
||||
│ │ │ ├── 360e95dd7f9428a51961a1f4ae7968a6e3101b8df00027432c6c743f64ccf9f8.json
|
||||
│ │ │ ├── 367d8bf55d8d85048571009c40b04ef43211cc9c630af6222fc5db65fb382cc8.json
|
||||
@@ -468,13 +501,18 @@ customerTicket
|
||||
│ │ │ ├── 36f5a5ada8b3f2cb753e47b20cfc0baf6e6fcb2fb67ac8d7df658ec8b82b34f4.json
|
||||
│ │ │ ├── 393b2ca571bf671f26f243678a1be73266fe7435c43d7c837f1b53d4dc54cc66.json
|
||||
│ │ │ ├── 3948717314f47afa835e760073df4764ac427f4858f5ec0ee6a2f298ac393d49.json
|
||||
│ │ │ ├── 39712339ec9b29be60c73ea29595c45b9e853f2facc2c4cad9864ecfce375f8b.json
|
||||
│ │ │ ├── 3b3003c9f49c174331e4f6eff0a43e09b9d64d0813c39e0c24805ca796d0d15a.json
|
||||
│ │ │ ├── 3b9252049dbf8ef67e64a756bb41325a6cf50190004d4442119d438620bacc72.json
|
||||
│ │ │ ├── 3b97506bb601aaab9825ae607b89cebbd3a1eb30a6176cb038baf9898ab0e4fb.json
|
||||
│ │ │ ├── 3c4a42b0b79b95acf81967479efdc4cd0a369e3b53b771fe87478d6327ddf0eb.json
|
||||
│ │ │ ├── 3ca58a3cda78344757f73fb9dc390d4bb3b45f4a711a1a058c049801282ec1f8.json
|
||||
│ │ │ ├── 3cec8055583c7d94e03ced535376a8bd9d3d335cd6c7e328f38dc2a9e6b1e397.json
|
||||
│ │ │ ├── 3d66d87daf2039a3fcd22d5d844e30c00443bed258dc048fbf939d97258e08d2.json
|
||||
│ │ │ ├── 3e4a861d0f7c53bd5ec082f319d974a4a34d5943bf2408a639d11e0ce9bfad46.json
|
||||
│ │ │ ├── 3f0cad4a4a6eb440a2b133bf72f2e72d6e7ef3668f4c8dbc2c759dbd1a41eb6f.json
|
||||
│ │ │ ├── 401b3df2aa1a5c618d917ae2422e552c59dfbc5220b312fe7977d751168f6987.json
|
||||
│ │ │ ├── 406e12ea55e61f7314b3b46df9e152e549fae319a2f71cbb01e7fe8bc4bde7e0.json
|
||||
│ │ │ ├── 408f6eb568e45e950fdc170776dabf9087829df0db630e16643fba8995318d26.json
|
||||
│ │ │ ├── 4177b5c9bcdd159b716f3e956852b337fc54e7e597b5631d0f0e9a4039011901.json
|
||||
│ │ │ ├── 41b1ac2b8ad040a3aa7173910cd19c2074adfdc23c4a1df31e445b61427a330b.json
|
||||
@@ -491,11 +529,14 @@ customerTicket
|
||||
│ │ │ ├── 4993b53d428cbbb1e334ea396aae6397586a1295a35f39da038cfe72f0b89fd4.json
|
||||
│ │ │ ├── 49dbf05487834af2e8fb3c1c4048f93565e6ca40251d04486a137b8f960adcac.json
|
||||
│ │ │ ├── 4a114c79b6523fc965e3aabcaf45df383f34f3fa942efed1aab21819d5a11cee.json
|
||||
│ │ │ ├── 4aac931923deb8a3de17b8f47d7ab8e72601213712c0e63b4e0d99f4adb7d4a2.json
|
||||
│ │ │ ├── 4ac6acdeea92befda1eef12058ee632897641929f6f8b3a6e85e4ef042a5f02a.json
|
||||
│ │ │ ├── 4ae084687e4372e69f349e8bbc5cf603768a032d3d9ee4d2504e6709ed8ee440.json
|
||||
│ │ │ ├── 4ae419f04a3a3b1221faef43b8526910f1958336162fbce432bab0e596ef2aa1.json
|
||||
│ │ │ ├── 4ae7b7ad5faee3292f9987cf047c1b9f484bb316d9e8c177dd85345bdfc861f3.json
|
||||
│ │ │ ├── 4bf46c6c0f27e14312f7c1311bca1bf5e1bfee29ea5cb35e6f94142141985d23.json
|
||||
│ │ │ ├── 4c924746bf7373f2f4b09200257622c6f97c2ba7fb3edc7f6595709e3c7ce9de.json
|
||||
│ │ │ ├── 4ccefdf6d084d96689cc37f74a4b38f2b6d357fa595c259864b087c2766cf7e6.json
|
||||
│ │ │ ├── 4cf64fe0b79aec18c49355f7d82ab04ae66067042a6c364e89842109427ffb5d.json
|
||||
│ │ │ ├── 4d244ac4c7ac7dddfbb2d8ad6e70b2f9ef0d5226d2660b2ef38ef109f19eacc4.json
|
||||
│ │ │ ├── 4e56ef4c75a0ef1d175a72a6980ba047bb4aa51d49b8a17471d8f342a0b779c0.json
|
||||
@@ -508,11 +549,21 @@ customerTicket
|
||||
│ │ │ ├── 54d567d298ddc3c274327011a29bd4f2f2cb31efaae5ecb3a7a94164a3eb3c1f.json
|
||||
│ │ │ ├── 55322e8bcc443a33b31b8e43e27a92b0c89335d5669d7046d34f58d33edef673.json
|
||||
│ │ │ ├── 57bc96866536f0d67300ed29e56bd0f45fa9119c787fac805f228adcab18dc6f.json
|
||||
│ │ │ ├── 57e592c703d95adc50236d5c01fd6314a85061e0969c885789c1045c6d9e3b7a.json
|
||||
│ │ │ ├── 57fe8325b25a8879793a5d96b0ed3d069c07b148f81bfc7c07da75ec353cfe76.json
|
||||
│ │ │ ├── 582c91a8ddabbea5521fc65d724e4fb3672c070f9213fae7b58595fcf62f3c85.json
|
||||
│ │ │ ├── 584447cf308657a696ef8465181b12d2c06f9dc5ff69752bea66bd0b9631feec.json
|
||||
│ │ │ ├── 591ef8879d355b6e3a0f7619bf275b8ab072a6e64b9c5a4b30fd892b792a2695.json
|
||||
│ │ │ ├── 592a03ef799b68757045867944b83c39e0d478ee3cc7eb3707d2cde946728943.json
|
||||
│ │ │ ├── 596a13e9b322f8c3332a3ae214ca7bf368ba6158ff3adcc7db4582f102ea51dc.json
|
||||
│ │ │ ├── 5a20a0814514ccdb84861bfc664f1659e9fb8f233dda858d580ca95df84c7c0f.json
|
||||
│ │ │ ├── 5a34ded8b2dd4c10c4251d287580918b88690c7058dd2299d2d670170104241d.json
|
||||
│ │ │ ├── 5ab2de631b0d480df2665ade93dae4732a4d6a1f44b809b2051ccff4696515af.json
|
||||
│ │ │ ├── 5ad9702c764b87123ef895aac69745b7d80fd436f00a94b4a729b0a8272f9edc.json
|
||||
│ │ │ ├── 5c57722fde215829c388b9376c72ee980ab6e267face197b6a950b67977cee15.json
|
||||
│ │ │ ├── 5c6ce39f232422db5e5f56206ab7c52a24c61d02077a6f57f32da47075a74d8c.json
|
||||
│ │ │ ├── 5cdbef00c015921b804d0a196937c35643eb7679fd70684d010a8fadf9b20b4d.json
|
||||
│ │ │ ├── 5cec06ceaec31a32af8019f455425a320b68f25ca590a728c56aee3706970211.json
|
||||
│ │ │ ├── 5d43f2c9afc3451745d2a1b5f1ed17bc138c9b19caff61beadbc40a4e970b525.json
|
||||
│ │ │ ├── 5f759ce7c1bcc67b3972da0ff15616dedf748eb2a85e029802d6f0e0feb053ee.json
|
||||
│ │ │ ├── 5fcd0fdc47a301cbd07130dfa7545017b5975c29b9d7458ec65938ed503506a6.json
|
||||
@@ -527,9 +578,14 @@ customerTicket
|
||||
│ │ │ ├── 656466a59cda75489e13531815b0b6075b27edf7d4261322324e5c621edbe8a1.json
|
||||
│ │ │ ├── 659df1da012e33aa3c92c4d36817034e6ec1a4d5d7ae94e6c2328f9aaa607b6b.json
|
||||
│ │ │ ├── 65c9b0921f2ad9b0228f5685c40d5b341da9604f8cec59339fded4cf0bfa40f4.json
|
||||
│ │ │ ├── 66eaa2742148e5f53050ad708d10eeecd741326cc5d983a1aa671dfc62d8afa7.json
|
||||
│ │ │ ├── 677cb6ace7f743e1db9787ea089a64c90f227c2a32db23acaa2105507ac28a2d.json
|
||||
│ │ │ ├── 6802a4e7d920a2a5ce48a61989565f6fa769c314a01b9179cb9d44653b9467ae.json
|
||||
│ │ │ ├── 6876d216f82a3a5b12bf9005b0658e7f8e996c95d3ac882bd38fb1e7d05d9001.json
|
||||
│ │ │ ├── 68ac29aaf1f897ccccd83e9f158a14b1ba6e1baef666230ae35de023dc6c30a7.json
|
||||
│ │ │ ├── 696bb846cc6a3735e085b502a1008e270903c81c7a7adcc7676abcf1930e926a.json
|
||||
│ │ │ ├── 69bef93e5bbc63880e88aca4535dc6a229a57edcfbc65fa829912a3bb847a6ff.json
|
||||
│ │ │ ├── 6a9ce83009b1228569506d7054e15c8c59b4fa9dceb9c15b9cafa288756a2d8c.json
|
||||
│ │ │ ├── 6bd49215f8d2739a9e365dcc8855b95cfa34352c067a9e9ff48a623318c4ddaa.json
|
||||
│ │ │ ├── 6c69494ba124d4bc564bcd3a3df1f33ae0ee0769001a2dfc46825ef8b44dc3e2.json
|
||||
│ │ │ ├── 6d3b094dc56f9b26ce7374ba28e460b85248711223cf516f8e732c5d8f4d9d75.json
|
||||
@@ -538,6 +594,7 @@ customerTicket
|
||||
│ │ │ ├── 6dc742b24111f73be6bd5c6d49bd013066a7bf4d299d145391fb0de1f4ed6fae.json
|
||||
│ │ │ ├── 6ee9154979fb497a8ec229a7177660af53a487e1aa17c7b555cdc67797615162.json
|
||||
│ │ │ ├── 7093cbd0a855750457814388b6e527fd3b612163f7a2b898ed6ce470dae74843.json
|
||||
│ │ │ ├── 712474bfe126be3bc31a5e719d9119149235071ef01e34cbe1e71ac49fd991ca.json
|
||||
│ │ │ ├── 72ae8a21c9106c639725190cc02f4e07efe7c645896cc4410923ad36751fbb2e.json
|
||||
│ │ │ ├── 72de23f55766289c980e6ccf604ca5d162e0989a222271b7496775ba4d749e3a.json
|
||||
│ │ │ ├── 7379dad88b1741f6f4d999fcea3eb0ac4ffe56f72c07dc11881e94615457bd52.json
|
||||
@@ -547,13 +604,17 @@ customerTicket
|
||||
│ │ │ ├── 77916c3b035a577b24da1a1dbcf47db7307af4e0774fecc549f7c95b91994a20.json
|
||||
│ │ │ ├── 77f3cfc32bc9234033fcc789ff327a4a98809057bd34d1539b7f2883ccf74ab3.json
|
||||
│ │ │ ├── 7817261e311bc9c5a0405245bad6d0f942a41193646a3d1112bb4b9fed70c08a.json
|
||||
│ │ │ ├── 7826ff204da4eb43ba178bc525d51a7b311100c57866aa0a2107be983c1f5883.json
|
||||
│ │ │ ├── 7975e2dc7e1d1eb2154b9aba8f19b6207acf85a245cdda0089c9dd0f80463ef5.json
|
||||
│ │ │ ├── 79ede3c31ce20a16af1a767ed248e4a7840803b6d34cf90f1205aa36f3acd7be.json
|
||||
│ │ │ ├── 7a886aec3fe4d432060a7ad6c88a9e0fd810a122ad1d244456af089a828af8c1.json
|
||||
│ │ │ ├── 7b25ccdfeab6d57509c1b8a9745fb601ab7e8fcae0e73a22932ceb725fd38c9a.json
|
||||
│ │ │ ├── 7b6916f499ae0ceae3eb3d509b51f42a7eaa98e43e161fe87c44b9a5bb01287b.json
|
||||
│ │ │ ├── 7bc90a6d89ccfaea18fd0aa05f2ab506d0639360f33413bfa9385ddf49e6d2fe.json
|
||||
│ │ │ ├── 7cb69c820b549230484188086c76dd8d20f68f682fa6345c879cb80d54e09849.json
|
||||
│ │ │ ├── 7cd278b7ea82af8777e4da2f84fd1773dae3dd371744f66bbe12c0b925fd6486.json
|
||||
│ │ │ ├── 7d276789b80c8d9d1677556b3fb33b2da05988e97f6bf28903a044bb9fd64894.json
|
||||
│ │ │ ├── 7de85cc2c6e58ead611e3ac76417ecd5316b7fd3f82ae70937c829eb809ea840.json
|
||||
│ │ │ ├── 7e136779a95d8e8e2ac6cad2192890254fd0d54d46b4f5514debba0f22618d75.json
|
||||
│ │ │ ├── 7e1c0a38a3ed90205d1829bdef528887ef12c928d4da797e06a79b3e7a1a92ba.json
|
||||
│ │ │ ├── 7eaffccd2a080e53d836bffdbe88facc2bfdde45ea602c9a800ee4e06c9f2f94.json
|
||||
@@ -562,41 +623,56 @@ customerTicket
|
||||
│ │ │ ├── 80468d8a995fb4092dfe5e57e25592c06713287d48367b675ae4d00955d4b25f.json
|
||||
│ │ │ ├── 806a3b07b690ca3871b1854aaff6f9042e48cf7aaa9bb9b6f15ab9634eae3b37.json
|
||||
│ │ │ ├── 80f142842848c4420eeec86959e77c24566467dbece6a275dd9c6da15f98551c.json
|
||||
│ │ │ ├── 81b1650d100b8e191c8ef7eef6f8e84e3881c1a2c0d08bed0c35751ee262c405.json
|
||||
│ │ │ ├── 81d5fbc2e91118aac67b18de1ab980f4dd81c8f7804a195e30017626db220ce8.json
|
||||
│ │ │ ├── 8288f4fa57d35d3d5e6b8c39227307c12362b9939e5b5a24447a786416b00362.json
|
||||
│ │ │ ├── 82fcc152dd67510406aa894d1e38939bac365093de9f34db4dfe2485806fda4f.json
|
||||
│ │ │ ├── 833fb89a8d72afc195b8f5bd7338109d8ce7be79f240dac31c4dcd36c8c674f6.json
|
||||
│ │ │ ├── 83ba45b41bd900a8e4b8c4f817e0c2a0098028ca7bbf7d7bc2514505ad25eeba.json
|
||||
│ │ │ ├── 84579b3014e1a83e4201862df1e93378f5f953fbb547fea30965e335be5e7b1e.json
|
||||
│ │ │ ├── 8493b4822bc3ed0ccd847d04d26598953d76d438e8381c1102922ac8530cd4d4.json
|
||||
│ │ │ ├── 84dedd4165eed360c4051690b2aa84f6ecc08f3f053f4129ca6f3e33c334ba35.json
|
||||
│ │ │ ├── 853fba8af82971f4fa56d957282d45d335d2e794f8e11001977161265af2ec79.json
|
||||
│ │ │ ├── 85797284a266529c7ce83ed46795c36a9e23deedd8d2676424d2ef2f0272b7ae.json
|
||||
│ │ │ ├── 8595a5d566d43428818ee229e64f6b56453cc9ca61fd68be0b1c3c783d326315.json
|
||||
│ │ │ ├── 86d14d3fd1c81cb7c809464cfcd0a4b47c691ae63ed379e4303d62a8f15d3332.json
|
||||
│ │ │ ├── 87bfe779b23e084cc44ff1152360e9a4b3a838f6b8b3cbb27cf503e98fbdb614.json
|
||||
│ │ │ ├── 885114f15b5e41946539500e8df7e848021aa7f62c5a5bf097176f7b1cc741df.json
|
||||
│ │ │ ├── 88ade181c6611ec33d9bcf5fe739dc809d81875b6571b9f60f87dafb4f5941b2.json
|
||||
│ │ │ ├── 89273d53a77489be59de43475ae2b053374f33b01b5400a963cffe0804427f7a.json
|
||||
│ │ │ ├── 894fa352b1f01de855ccc8a73926cc5ef33e6900580bf45668ea823fd2a69998.json
|
||||
│ │ │ ├── 899b16856382d32c4e5b39d02cb251bdedaf35c5a81bede5c19bfaff092208e9.json
|
||||
│ │ │ ├── 8ab55a4f4f776ea75d01aaafdff3502c07b5b86e359789b22c590c7f988bf5f4.json
|
||||
│ │ │ ├── 8af38f9544549f250eff9b83a6c2a0e51f86d6ac51de0db24b48b7c654723a39.json
|
||||
│ │ │ ├── 8b92443bbe1ebe0a8534d065ce886b14e563f6d55f6a3c8b3457c78230d55fa5.json
|
||||
│ │ │ ├── 8e4cb8d07f4d43e171550ff2fc9423efd032b911d7ffee51259e437d4dc32d4f.json
|
||||
│ │ │ ├── 8f6a7f00c0a8c66241db8c1450cb431107dea30bf65b8e13783ce7f976d98aac.json
|
||||
│ │ │ ├── 8f763b17555f00e8d20ed1ea8e9a065526f0e1fe942e70a62112c13309d78bae.json
|
||||
│ │ │ ├── 91c2dc58207c4311391f22d524f461032f7ef3c9443d8f584edb2128966821bb.json
|
||||
│ │ │ ├── 91d83c62f39f219ccb6125502bf8e0a71a587dfc0a66d977a9bb93c8f2dc5dc6.json
|
||||
│ │ │ ├── 91f4a9afed43fafc3d99d7fe5eaa945cd5c89e29d4d6f6448b630c3e58db05e8.json
|
||||
│ │ │ ├── 9301321aa7fb1a7beff5518eb54fbf1bf7145e0aa76d3ed29ae4cb38f1057da9.json
|
||||
│ │ │ ├── 9326d87e36b2dc24e6173aa3346f46e7df9fc3353fdb409304af4363c286a41e.json
|
||||
│ │ │ ├── 94ea5a4bac836b2d19b08e446cf90eaea49b9eddc2591d261c384b3513e36f26.json
|
||||
│ │ │ ├── 95dbd272df1d7fd18ba53b86ffda71c7a57046e6a303b88683e323e80ced3a91.json
|
||||
│ │ │ ├── 9697c37cf3a88d2b4eb5419b2381951a9aca966172316db79e7b4c32557d5e87.json
|
||||
│ │ │ ├── 97506546de142970349e57e8a14064280e66a4fcdbced5dac55fc263630a2247.json
|
||||
│ │ │ ├── 97d8ebb860cbcd63be9ef80c6e2094e4a7ccba668b291d3c4815133d5fb8f37e.json
|
||||
│ │ │ ├── 980f1be05d605b9093619724462c9881a8be063a5439567866172a35f8d22316.json
|
||||
│ │ │ ├── 987273442dbbf3bc6bc7f6e1223b147341c5528999fa9ed8ebadfd18d2369908.json
|
||||
│ │ │ ├── 98ca8ab4bab0509a82824f0275ca7d1e9e80f5c8189579ae410a115c62126652.json
|
||||
│ │ │ ├── 9931a5b7d7ab58794d003cce1d0719c201c623a844c7f834f66c88e9390c824e.json
|
||||
│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json
|
||||
│ │ │ ├── 99f94b7b3b60f627c482cc195709b98cd2faa55f14c19c4bf5e0af8a88f34b7a.json
|
||||
│ │ │ ├── 9b267b0c45b93cecc1294ca1e040e5a18fdc9e0bee69a0ca8571ee11becdee97.json
|
||||
│ │ │ ├── 9b8bd2aff50e08b6f053b74a409c9439db41e692e29aabca5b67057884d945af.json
|
||||
│ │ │ ├── 9be5120f28278ca0713aded1ab546d4d4c7074572b49d9d273a48c0899308a57.json
|
||||
│ │ │ ├── 9cac12079d625e44ef3f62c6e4c9c7848f1cdd4d086c9251b3c81101cf4c114e.json
|
||||
│ │ │ ├── 9ce16f69c6b04f703adb24ec7498a2d2ecbf6f7524c459deb25111247e550698.json
|
||||
│ │ │ ├── 9d0e2a1fb14529aabd31c82c8d8f5543c0068366932934038ba93e234f291e22.json
|
||||
│ │ │ ├── 9d3a68c9991464131299b924086faf1e0c474c156019d681ba41e64d8100a7b1.json
|
||||
│ │ │ ├── 9dd5b6a7f155cef3a16a5dc01a1f0cbd458d129e379b93566b68f9c366f21aeb.json
|
||||
│ │ │ ├── 9e65c0ec94144c1216a5885bbaca12e5d5b028ce97730aab0d740a2fdf3b41eb.json
|
||||
│ │ │ ├── 9ed5e291859abba38b07725bc6cf0275a1548e937888a5ba177674afaa48b96f.json
|
||||
│ │ │ ├── 9ef0e904ba6d1b295c75a1d5741851f571339a8e22c7de96bf5e2508f764ee43.json
|
||||
│ │ │ ├── a04a8ed5ca0ac9661dda4f6f369248937cf67e61cc982a1f17b24495263d53d6.json
|
||||
@@ -609,14 +685,17 @@ customerTicket
|
||||
│ │ │ ├── a4a27e7b3842713c361895d6d9c5fc9e4341cce71c5842f014748e1c1c23a197.json
|
||||
│ │ │ ├── a4a568f58efea076f16698bb9bb6279a92ab44167a6e97d424fff44900ff973e.json
|
||||
│ │ │ ├── a5231d68fe35e6b02ff4ff70e02a3229c08d9b6c2bae774a1b3227823b95e15a.json
|
||||
│ │ │ ├── a558b874a8c8e3baac9b3c2f6c0aa82abe95057cc86115aa8ba60cf2b0965abf.json
|
||||
│ │ │ ├── a614593a975a3af1612ede797f69fd29220ee9251ab016d34d5e612a04696ce9.json
|
||||
│ │ │ ├── a698382a7159e40294490d6f668a46485f1a08fafdfbc52bfc616f362dc084a9.json
|
||||
│ │ │ ├── a71938b441036f6fe7e7723ee3427689a11f834e383a250ad2868b340aee216b.json
|
||||
│ │ │ ├── a7729f0b6bf20ded62ff7b6ee678fdaae9516dcd80c2a8f9e9f3d9299f6e22a0.json
|
||||
│ │ │ ├── a7b036e5bd51876694ebd883bd1b0a527c5f8409b48d4716612311e954a8cc0f.json
|
||||
│ │ │ ├── a8274003d8d4ef62b23c66e3baecf14eea260f99a4509411d58506f560c2b474.json
|
||||
│ │ │ ├── a836a06de0905683ff2aee057760fe6e4a3f9161d0cb60544fb7afc9ce782671.json
|
||||
│ │ │ ├── a93b84a0def4d2e0e97ff35032871b0ce64786ff468912aed21fd958b9fe782b.json
|
||||
│ │ │ ├── ab1648cc3b4d4bba5754e19eeb80ea160fd20207d5530ac371647168f9ed211a.json
|
||||
│ │ │ ├── ab90a45c416c5da4e2e4506ea08cee8fd010acc608a718fee1985213112bf776.json
|
||||
│ │ │ ├── abaaf1c3e88b939bf1833294f5233fa8e7c94091077495c424a0779298cc885d.json
|
||||
│ │ │ ├── ac454f6bfed5913148eb8c9caaa0e30f99bac381936b44612bdee8274bb27b29.json
|
||||
│ │ │ ├── ad1175b041ab42a2615856a41fbab394ae63ef9dd991901c0658c3fb0846ad3f.json
|
||||
@@ -629,7 +708,10 @@ customerTicket
|
||||
│ │ │ ├── b137ac0b92c8567809fb0323cd320fbf60c45ae6fad1bbdfb10d6efb22989f0b.json
|
||||
│ │ │ ├── b390f7bae9f80c48e703a6664d9de6ec9c141cdf8ac2f012d67ac2b4d8a25b4e.json
|
||||
│ │ │ ├── b396b497b11e135cfc19ccbee84e3e67df0e401a412f51c0336c89cb3609b5f5.json
|
||||
│ │ │ ├── b3b223f1a3b058d17a3c0a0e8cb95b4ff2cd54bc57bc5f57f0fd7132919e7007.json
|
||||
│ │ │ ├── b3f59496efa1bcbf6d511580880b5dff989b379977f7649d1a59c73566a35532.json
|
||||
│ │ │ ├── b4446ca11a1f4f6ea45256e755ed56d6ba7c330278e15ead8cae2de28d5e18d8.json
|
||||
│ │ │ ├── b5b7236fe4db451727eafb0dc2e904fd860f9000b44f898ce55f8ac8709ecee4.json
|
||||
│ │ │ ├── b6e731246dc10854a0fa0455c3c5816e29887b1a8c436d518926e1c2a5e36239.json
|
||||
│ │ │ ├── b7014ccf0ecd3cdccb86d9ac37bd58c5ca00b59d2de58a194eb66ac139e14a5c.json
|
||||
│ │ │ ├── b7ddd6ec7284c45f0925a557b161afd7c9d01171b06c3f723ac4da599ecf47d2.json
|
||||
@@ -637,32 +719,48 @@ customerTicket
|
||||
│ │ │ ├── b96845015841d34aa0edcc02a6a51a4ec5f77bd6827d5e9bf8ca6f620c8de3dd.json
|
||||
│ │ │ ├── bb3d09cc5d1bc380e841e2eb19cb497391117101c73c358e6db03db4d6ae51e7.json
|
||||
│ │ │ ├── bb78c0a0d3cbee2cd3238fcc9a1c14cb8bd9ce97a78de60af3aa49f132f966a8.json
|
||||
│ │ │ ├── be227af94c0ffcf8abcd019a59577ad2aa5807dffa85ff8c1e8ef2e8de5ec8a9.json
|
||||
│ │ │ ├── beab35cee5ff6b070f62aef30a8db07392e21e64dc733c643575a0c6a7387ffc.json
|
||||
│ │ │ ├── bf759ce9656625911599b351f64028b9bf0bcdcf4adc084e96e9cccc80bbe50c.json
|
||||
│ │ │ ├── c1060dca2a3f437f386324975be659215fe9f7e689137dc85e823879b462d11f.json
|
||||
│ │ │ ├── c10793a36312ddf2bd8ed9edbae5d67a19f54b78ac3cae01682d38c34f2bdf11.json
|
||||
│ │ │ ├── c199fcf03e3df0de15c0e7fa5f89fbd9649f2be4a257c8de9a7ebc69ce0d629a.json
|
||||
│ │ │ ├── c1a36d35264d786572e00bd20a4d62c18000e4a5a744b4bbd585c66ba2b1afa4.json
|
||||
│ │ │ ├── c251d835538bf3c08a96268c8b1feaae8084a9c298d634ce2f2ee3b4b15188ce.json
|
||||
│ │ │ ├── c33b2a3b22df6e5ecd305b22e306a3f973d17e2de01ce41f481232b6a6347fd9.json
|
||||
│ │ │ ├── c44dee69553ce27c5bfc14eea713c9bbd02b95f34f6461394e52ddf160771f4e.json
|
||||
│ │ │ ├── c478d961844d415bd36c75ab4921d7bafdc9419a9e0bf6a1efc44f14130e50fe.json
|
||||
│ │ │ ├── c4878d8d8ae19e705821ae40afc1ede4be1a64b559dfdf89f898cb4383718c93.json
|
||||
│ │ │ ├── c4a56e0890c5b5bdc963949bff54e8a268081652516ec17e155781a93c56b390.json
|
||||
│ │ │ ├── c6c7b21caf5043dfdf6c25863bf23da656645640cf2ccd914c223f8984a1b3ef.json
|
||||
│ │ │ ├── c8e562a594b215d21999461d5c5fc0894fef0eec4ed7a7ea5b975df305f3d53b.json
|
||||
│ │ │ ├── ca033fb139fdaaaa0d083b9189e67868f0da87d0f954a863d2031c5955075e8e.json
|
||||
│ │ │ ├── ca0690b4cca9539d0bab5c0347df64d6b31883da40092497211663d64e7448a4.json
|
||||
│ │ │ ├── ca42945436e914d9530e20dc780794451ec6ab51e17414e8c9107ccad3125fd5.json
|
||||
│ │ │ ├── ca8e9d2e76f32d42a0da88c9477956b47dca6cdb27504d8d126e0075306eae98.json
|
||||
│ │ │ ├── caac8d56f4ba7b3987d86c31357c6ffd2c4125d16e4637cb5a4568ac6eee29de.json
|
||||
│ │ │ ├── cabbb48bb45d3c5475441362567653dafcb896c08b6b039f08f2ca0fa2c64991.json
|
||||
│ │ │ ├── cb87cd70f59a8e0950529ae42a2229a259ff8b305e4319422fb1a9dbc41e63e2.json
|
||||
│ │ │ ├── cbb5422715d1c05445a1d36fec9c09257e89393d12a192448cc7615f2873ba1b.json
|
||||
│ │ │ ├── ccd15ad60b63cd29e2bd2762133a74e2047a2894fd67206dff7f7c7fd082b399.json
|
||||
│ │ │ ├── cd137e2201aa02abbd8d57e28535e25842bbbe227f8e859dd45752d1e4217b0a.json
|
||||
│ │ │ ├── cd4804092002a20d5ed88c32b577b0868a0ef39e518c749dbadec455775917fb.json
|
||||
│ │ │ ├── cdad1106fd648fc1148b348097481d04bdf5b311be1fe4edd5274162f9348496.json
|
||||
│ │ │ ├── ce52c1ffbf8db0d0b4ba85e0a07055258a8eb179db097f1df874e5be81aae49c.json
|
||||
│ │ │ ├── ce640fd27445864e6f2c38659c24f873a702c7a669a2f0b55df81d62ce26b4ed.json
|
||||
│ │ │ ├── d083dc75811629ffa77ea30c1965e67ef3bc2bf65e5b20a1a35cb0f80a46949c.json
|
||||
│ │ │ ├── d0d623b82da6c3e0f37522e8ddea038c4a1415d50059372915009dcee6628ce1.json
|
||||
│ │ │ ├── d188f9185e71f8910607d275d82fea22199ba0e6009a5b1e34e5a20f041f0d71.json
|
||||
│ │ │ ├── d2513d4f5ff7e6088864ff497613f47d4083c1ca378649a3f1db6d45a3f3fb2b.json
|
||||
│ │ │ ├── d26a8fc663e9e5fcf33c87592f8c0456e7fe9ec3390bcb902631144d455c2922.json
|
||||
│ │ │ ├── d449c32636f32d9267c66232be6123566cb92d0352de164bd4b01343c1ce75d7.json
|
||||
│ │ │ ├── d54677c4320658676f86c09348933f298192a048b637aef94d607eea8a92b474.json
|
||||
│ │ │ ├── d55e8987ca5ff35748b62290dc14150b1df53163c6f5a5cbe19d03058368f1c6.json
|
||||
│ │ │ ├── d561ececcdd4e7ff59595389fa499591f4e7f3c5324a58dbdd749ccf29e90cd1.json
|
||||
│ │ │ ├── d5dec4474de6c46020e987f20aa3fe54c9bb9c6726a067a75ed94091d552b87f.json
|
||||
│ │ │ ├── d6e446c4c97c9aa2b6efbd6042496ca45d8403aacb591adc76e2454b959bc725.json
|
||||
│ │ │ ├── d8152167c95e1161f8d981a4f767c1a33aa219c909ec459eeec1060c662d201f.json
|
||||
│ │ │ ├── d8ab4ff8856af7ebd9b4f58636afbfc5e6fb185936cad3e6b6446baa588b8f48.json
|
||||
│ │ │ ├── d8c4256d4b9c925575ecabdd7e45c9c95e6fdb32bcfd2d9bb50a05ede9365ece.json
|
||||
│ │ │ ├── d8cda86cbe3f42bb98bb0097071d95924950dfc6ed65a7075d3b22ff1ae0bf0b.json
|
||||
│ │ │ ├── d9516c3232e0f78225764bf4f9586c88932df925c45de71f4eb1c883a9ed5b5d.json
|
||||
@@ -672,15 +770,18 @@ customerTicket
|
||||
│ │ │ ├── dbaef2f4942a9f7ecde0afc10e4eebfe3a2809af1d8b76cb036bd417f19274b1.json
|
||||
│ │ │ ├── dc90ac5e8d6766f8001bc2a40d21c203717d7a9f469956c2f6d9bedf3ae51aea.json
|
||||
│ │ │ ├── dd795078450fe69f61a445d3cf1275420be57381b005597cb5003859b2025800.json
|
||||
│ │ │ ├── ddba9bc111e6e3e5e7e8238e60f9701ae68526765935f020b390f7934bdb148e.json
|
||||
│ │ │ ├── de1655ae1d42e56f0cdb8788c0f6ba1c7c6e6725e6ef5e80969b92ec3e68ac78.json
|
||||
│ │ │ ├── dea57fa4a89ef99a815bd9b1bb71b8475727a3d7d88161a9a696ebf636f29ed0.json
|
||||
│ │ │ ├── df2d803fc69084e91df819bda27a7e9f7303a40ff7a5d30d93fbc16daf5e2d45.json
|
||||
│ │ │ ├── dfc9279b0d9023afc1dbe55536fc1cd7a556a6a437775128b9e41615431b4733.json
|
||||
│ │ │ ├── e2f4f5c0bcb971ba3cb08a4ba14ed5d1ba6b5899fd36db1f5dcdbd2313cd318e.json
|
||||
│ │ │ ├── e359eb0e19f76b69decc0ec747a7fb4c9843046bda31c5e45841858e01bcb5a7.json
|
||||
│ │ │ ├── e37b6b5144067b3650a3e1045655f367e7027ac2f97a8d8c19ce2e5c5862cea0.json
|
||||
│ │ │ ├── e42f6a96ad8938a6ab248b342feca7ec6ecf2a734d25141f62dbc9a6c52ecf6b.json
|
||||
│ │ │ ├── e43b7eb1fd2770d280dedb21e9a453279f3aa8262b2a0409fffe8fa9f7016880.json
|
||||
│ │ │ ├── e53a0dfeecd1b547e38896f8e7920e6801d84dbf61bb4513541892d44d42ae99.json
|
||||
│ │ │ ├── e5d0ae5edd59d401880bd9bec4f531c4773cbd53d6f3587b55ff6f29cfbc5a75.json
|
||||
│ │ │ ├── e6b4123e64b20d577378853fe71efb3743dbe9aff24ca06067b4a2c1c2efc53c.json
|
||||
│ │ │ ├── e7c0ad70ad7e7f3a1f6f35e55f4cf87f6c0850abc63de71a418e81b88ec826b7.json
|
||||
│ │ │ ├── e810aa586fa2e56d1c31e6940e0e10616ee52ab4bf328e928e0beabfcecd8fb1.json
|
||||
@@ -688,14 +789,21 @@ customerTicket
|
||||
│ │ │ ├── e8f3b285df9ff4d30c78eb1eded3eefdd86cfe325a7c8c29fb8457b6dde5e712.json
|
||||
│ │ │ ├── e90aa709aa08fcf3294352b588ce8c8c902f3ef59a503d4b8d9a292a9c478096.json
|
||||
│ │ │ ├── e974cd0006d8160a6ef1316083bac9e699adfd87acc7edb404ae7eba1c799b68.json
|
||||
│ │ │ ├── e99791dc7ccf0fde796771290e06e6ea13ecf4d24044945c735f3e9f470934fd.json
|
||||
│ │ │ ├── e9a8155f1a3546e434485a2aca7a04c6857071bfcfd5a7317a62472d90919b9d.json
|
||||
│ │ │ ├── e9b4ac230d46fa80048827a24c4211b7a71a741f2997ffaf0921b25a40f505c5.json
|
||||
│ │ │ ├── ea3c12254259478e0d71c386203dd7263d9f836d6a6489481bc0deff9b69dc62.json
|
||||
│ │ │ ├── ea56445d1314587e18b1992d9599da03e1419ea7bc4929854b9039d6947de33b.json
|
||||
│ │ │ ├── ea8564b376c7156927c3f412f1a7173248d6b2fd7dcfc3492c4c9d7c44dae889.json
|
||||
│ │ │ ├── eb1dd65efa5a99811ccca80ff03a73cb155ad86d13635010a44c09c13953604d.json
|
||||
│ │ │ ├── eb782e7a5b5695997a5bf628a22f4d36f5ddb886e5bdf4b2f4ae53ecc46fbca1.json
|
||||
│ │ │ ├── ebc76bd945a2106c6bbe01b3cc09eb40e65b08aedd7c49709a38ad63511082ad.json
|
||||
│ │ │ ├── ec7939ed122c473ca1a4eee50f1c564e04e95ddced18cca56ed83efe5d4071dd.json
|
||||
│ │ │ ├── ecb32b5c560f5a4cad0801cd49c9a21fe4d19d7638eb5382b73610878c360d00.json
|
||||
│ │ │ ├── ed61ddacfc8c05f7392be053291989b55a01c1bfa5a1dbebbc4bc6e706370f53.json
|
||||
│ │ │ ├── ee19f3245c11f658fb60d32050a8bc4864ea289071ff5f829717ecacbac1d3e4.json
|
||||
│ │ │ ├── ee89093aa638ec9d38b76ba7adb66c8424d941d79deba2913d3d7085c1a062f7.json
|
||||
│ │ │ ├── effec069fa1e0bba5e3b055368ccbb033f964a968890d7efe7dc3fa9ae4f9c0f.json
|
||||
│ │ │ ├── f06f026d4fb736e9628a73bfaa9ccfe00efc006be79cb1c27f4a77ede5874f52.json
|
||||
│ │ │ ├── f12416bacd2eea21842fe89fcfc2c02ffe84619d7ce380d13679b64c6ec7496d.json
|
||||
│ │ │ ├── f2069429b170b19df531b051a2f5fa81854a7bedf1a036aab1a46c89a9ba4c92.json
|
||||
@@ -707,16 +815,22 @@ customerTicket
|
||||
│ │ │ ├── f4780f9612e45a5bf32eb07fd006b874586bf10ee1528219fcad4f91304a6640.json
|
||||
│ │ │ ├── f48a1f4157bafecbddf10702c6bfd0ee33addb5752a4eddcdaac7b7e0cfa06e9.json
|
||||
│ │ │ ├── f4bd364e7dd2fde53b658ae0635bf5143f1f4cc5eaa22b1ad9449abcf98a9bd1.json
|
||||
│ │ │ ├── f4e19fc3b788a0bd2cd1aa88764b3165055977e9dea5978c244dae73e19a6309.json
|
||||
│ │ │ ├── f542bc3aa4ea54e09e2fb68ecc937eed71e4307283d9cf7c2d94755c6937b7ad.json
|
||||
│ │ │ ├── f576af4a0cecb6cbaaf8afe5097913be0b7071e200fc924c918ea93446f00c91.json
|
||||
│ │ │ ├── f7c6e4b29e57cd7348c83e0544f38ed0514c634ef818d05b8dd6c91d5d6d3470.json
|
||||
│ │ │ ├── f7e200f94ac2080e0885bf4a03e8afdb5cea2684a31a7c09e67dbf2f4fee6f51.json
|
||||
│ │ │ ├── f8769e3eeb23aebc01d72869a2bc6e635d7dc82440bc2b0b0e11e6b6b17f5109.json
|
||||
│ │ │ ├── f89b36ddc835209adf7f9ecac43ec5b77df5f85611115e1b0b7fc7c16eae06e9.json
|
||||
│ │ │ ├── f8fc55a028176d1ea9a1b580a8653d4c45ae7466445f37e6c23867f12821c44c.json
|
||||
│ │ │ ├── fa27401c5036759a68a650cf8097dd25e845aa3c385c1425fb2603455e6e9220.json
|
||||
│ │ │ ├── fa76a38590131d290db328017ff4708f3f53db3570d0a52bc8f347f42b25e1a1.json
|
||||
│ │ │ ├── fadb933ee40737a7c1a11722b8c07663038f72148bd0e2c435ef319958670d9b.json
|
||||
│ │ │ ├── fb9736a4ef3aca74573f131c7cbd9f015d88dd755ffba3edd324aec95149175f.json
|
||||
│ │ │ ├── fbe224671138a9d843def175a438533c3d6cc144b2851f8a723e505918f2c22b.json
|
||||
│ │ │ ├── fca6b7e4c9f92410b2590539af80d6b499509255af4ef88282912c6c79a09330.json
|
||||
│ │ │ ├── fdaee4458f2806e2ccd16ae35dba0c0b6859ccbca419c386bc9d5f09407784af.json
|
||||
│ │ │ ├── fdb9c3b20a8f0496c09bb85b9b6df18cafb3a8655c68f103a1061823fb159d68.json
|
||||
│ │ │ ├── fdd30ae7a839a46eb2f57681444d93258df5cbbdc40d2c24aae4caa8adef0936.json
|
||||
│ │ │ ├── fde577f1231f9b033560bfe075896efc6bf8d6079a030a159035584c7d39843b.json
|
||||
│ │ │ ├── fe032050843e4e1c28f5712205df74fa1698390169658c4ddc4555b6bcce8872.json
|
||||
@@ -897,7 +1011,8 @@ customerTicket
|
||||
│ │ ├── query_20260917_060706_47ef388c_اصلاح_رابط_موبایل_صفحه_جزئیات_تیکت_کارشناس_و_magic.md
|
||||
│ │ ├── query_20260917_063554_1824eb56_پس_از_باز_شدن_کیبورد_موبایل__کادر_نوشتن_پاسخ_تیکت.md
|
||||
│ │ ├── query_20260923_131543_e5730a0b_how_do_staff_ticket_sms_status__failed_delivery__a.md
|
||||
│ │ └── query_20261006_111656_eab3ac96_how_do_payment_approval_policy_and_receipt_approva.md
|
||||
│ │ ├── query_20261006_111656_eab3ac96_how_do_payment_approval_policy_and_receipt_approva.md
|
||||
│ │ └── query_20261007_080904_8f4d683c_payment_notice__bank_account_snapshots__short_paym.md
|
||||
│ ├── reflections
|
||||
│ │ └── LESSONS.md
|
||||
│ ├── .graphify_analysis.json
|
||||
@@ -912,6 +1027,7 @@ customerTicket
|
||||
│ ├── .graphify_semantic.json
|
||||
│ ├── .html_semantic_extract.json
|
||||
│ ├── .vocab.txt
|
||||
│ ├── before-receipt-alerts-20261007-133102.sqlite3
|
||||
│ ├── chaharsu-home-mobile.png
|
||||
│ ├── chaharsu-prototype_server.py
|
||||
│ ├── chaharsu-ui-bank-new-desktop.png
|
||||
@@ -939,9 +1055,12 @@ customerTicket
|
||||
│ ├── customer-payment-receipt-320.png
|
||||
│ ├── customer-payment-receipt-375.png
|
||||
│ ├── db-before-announcement-fix-20261004-115635.sqlite3
|
||||
│ ├── db-before-deposit-normalization-20261007-113436.sqlite3
|
||||
│ ├── db-before-payment-links-20261006.sqlite3
|
||||
│ ├── db-before-staff-sharing.sqlite3
|
||||
│ ├── db-pre-chaharsu.sqlite3
|
||||
│ ├── deposit-sharing-live-1440.png
|
||||
│ ├── deposit-sharing-live-320.png
|
||||
│ ├── graph.html
|
||||
│ ├── graph.json
|
||||
│ ├── GRAPH_REPORT.md
|
||||
@@ -960,6 +1079,12 @@ customerTicket
|
||||
│ ├── notice-page-staff-1-320.png
|
||||
│ ├── notice-page-staff-2-1440.png
|
||||
│ ├── notice-page-staff-2-320.png
|
||||
│ ├── notice-sms-live-1440.png
|
||||
│ ├── notice-sms-live-320.png
|
||||
│ ├── payment-feedback-live-0-1440.png
|
||||
│ ├── payment-feedback-live-0-320.png
|
||||
│ ├── payment-feedback-live-1-1440.png
|
||||
│ ├── payment-feedback-live-1-320.png
|
||||
│ ├── payment-link-form-1440.png
|
||||
│ ├── payment-link-form-320.png
|
||||
│ ├── payment-link-form-390.png
|
||||
@@ -978,6 +1103,10 @@ customerTicket
|
||||
│ ├── payment-review-staff-1-320.png
|
||||
│ ├── payment-review-staff-2-1440.png
|
||||
│ ├── payment-review-staff-2-320.png
|
||||
│ ├── receipt-alert-admin-live-1440.png
|
||||
│ ├── receipt-alert-admin-live-320.png
|
||||
│ ├── receipt-upload-live-1440.png
|
||||
│ ├── receipt-upload-live-320.png
|
||||
│ ├── shared-ticket-metadata-1280.png
|
||||
│ ├── shared-ticket-metadata-390.png
|
||||
│ ├── staff-inline-desktop-closed.png
|
||||
@@ -986,6 +1115,8 @@ customerTicket
|
||||
│ ├── staff-inline-mobile-closed.png
|
||||
│ ├── staff-inline-mobile-history.png
|
||||
│ ├── staff-inline-mobile-opened.png
|
||||
│ ├── staff-payments-live-1440.png
|
||||
│ ├── staff-payments-live-320.png
|
||||
│ ├── staff-sharing-desktop-audit.png
|
||||
│ ├── staff-sharing-desktop-message.png
|
||||
│ ├── staff-sharing-desktop-ticket.png
|
||||
@@ -1002,10 +1133,15 @@ customerTicket
|
||||
│ ├── verify_chaharsu_live.py
|
||||
│ ├── verify_chaharsu_ui.py
|
||||
│ ├── verify_customer_finance.py
|
||||
│ ├── verify_deposit_sharing_live.py
|
||||
│ ├── verify_identifier_ui.py
|
||||
│ ├── verify_notice_pages.py
|
||||
│ ├── verify_payment_feedback_live.py
|
||||
│ ├── verify_payment_links.py
|
||||
│ ├── verify_payment_review.py
|
||||
│ ├── verify_receipt_alert_admin_live.py
|
||||
│ ├── verify_receipt_upload_live.py
|
||||
│ ├── verify_staff_payment_sms_live.py
|
||||
│ └── verify_ticket_notices.py
|
||||
├── production-seed
|
||||
│ ├── data.json.gz
|
||||
@@ -1046,7 +1182,8 @@ customerTicket
|
||||
│ │ │ ├── deposit-identifier.js
|
||||
│ │ │ ├── payments.js
|
||||
│ │ │ ├── receipt-amount.js
|
||||
│ │ │ └── receipt-form.js
|
||||
│ │ │ ├── receipt-form.js
|
||||
│ │ │ └── receipt-upload.js
|
||||
│ │ ├── attachment-uploader.js
|
||||
│ │ ├── chat-coach.js
|
||||
│ │ ├── contact-group-selector.js
|
||||
@@ -1096,6 +1233,7 @@ customerTicket
|
||||
│ │ ├── _linked_receipts.html
|
||||
│ │ ├── _notice.html
|
||||
│ │ ├── _notice_shortcut.html
|
||||
│ │ ├── _notice_sms.html
|
||||
│ │ ├── _pagination.html
|
||||
│ │ ├── _receipt_field.html
|
||||
│ │ ├── _ticket_notices.html
|
||||
@@ -1255,6 +1393,9 @@ customerTicket
|
||||
│ │ ├── tests_e2e_test_payments_e2e.py__test_independent_receipt_and_two_person_approval
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_payments_e2e.py__test_notice_identifier_generation_preserves_manual_value_and_customer_context
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_payments_e2e.py__test_staff_issues_identifier_notice_and_customer_sends_linked_receipt
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
@@ -1297,6 +1438,9 @@ customerTicket
|
||||
│ │ ├── tests_e2e_test_pwa_versioning.py__test_pwa_registers_versioned_worker_and_matching_cache
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_receipt_upload_e2e.py__test_receipt_upload_progress_processing_and_single_submission[link-320]
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_staff_operations_ui.py__test_dispatch_report_presents_delivery_and_engagement_professionally
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
@@ -1327,9 +1471,12 @@ customerTicket
|
||||
│ │ │ ├── magic_link.py
|
||||
│ │ │ ├── message_reads.py
|
||||
│ │ │ ├── notice_deletion.py
|
||||
│ │ │ ├── payment_feedback.py
|
||||
│ │ │ ├── payment_link.py
|
||||
│ │ │ ├── payments.py
|
||||
│ │ │ ├── receipt_upload.py
|
||||
│ │ │ ├── sharing.py
|
||||
│ │ │ ├── staff_payments.py
|
||||
│ │ │ ├── ticket_notices.py
|
||||
│ │ │ └── tickets.py
|
||||
│ │ ├── config.py
|
||||
@@ -1345,11 +1492,14 @@ customerTicket
|
||||
│ │ ├── test_mobile_ticket_viewport.py
|
||||
│ │ ├── test_notice_deletion_e2e.py
|
||||
│ │ ├── test_organization_permissions_e2e.py
|
||||
│ │ ├── test_payment_feedback_e2e.py
|
||||
│ │ ├── test_payment_links_e2e.py
|
||||
│ │ ├── test_payments_e2e.py
|
||||
│ │ ├── test_public_ticket_workflows_e2e.py
|
||||
│ │ ├── test_pwa_versioning.py
|
||||
│ │ ├── test_receipt_upload_e2e.py
|
||||
│ │ ├── test_staff_operations_ui.py
|
||||
│ │ ├── test_staff_payment_sms_e2e.py
|
||||
│ │ ├── test_staff_sharing_e2e.py
|
||||
│ │ └── test_ticket_notices_e2e.py
|
||||
│ ├── js
|
||||
@@ -1369,12 +1519,14 @@ customerTicket
|
||||
│ ├── test_payment_links.py
|
||||
│ ├── test_payment_notice_concurrency.py
|
||||
│ ├── test_payment_notice_deletion.py
|
||||
│ ├── test_payment_receipt_alerts.py
|
||||
│ ├── test_payments.py
|
||||
│ ├── test_permission_architecture.py
|
||||
│ ├── test_persian_text.py
|
||||
│ ├── test_production_seed.py
|
||||
│ ├── test_role_workflow_regression.py
|
||||
│ ├── test_staff_inline_sharing.py
|
||||
│ ├── test_staff_payment_sms.py
|
||||
│ ├── test_staff_shared_queue.py
|
||||
│ ├── test_staff_sharing.py
|
||||
│ ├── test_views.py
|
||||
@@ -1386,7 +1538,6 @@ customerTicket
|
||||
├── .env.local.example
|
||||
├── .gitignore
|
||||
├── .npmrc
|
||||
├── .test-db.sqlite3
|
||||
├── AGENTS.md
|
||||
├── alghortim_shenase.py
|
||||
├── db.sqlite3
|
||||
|
||||
@@ -88,7 +88,7 @@ def _send_sms(job):
|
||||
if job.event_type == 'PAYMENT_RECEIPT_RECEIVED':
|
||||
from apps.payments.receipt_alerts import send_receipt_alert
|
||||
return send_receipt_alert(job, sms_provider_factory=get_sms_provider), None
|
||||
if job.event_type in {"PAYMENT_NOTICE_CREATED", "PAYMENT_RECEIPT_APPROVED"}:
|
||||
if job.event_type in {"PAYMENT_NOTICE_CREATED", "PAYMENT_RECEIPT_APPROVED", "PAYMENT_RECEIPT_REJECTED"}:
|
||||
from apps.payments.notifications import send_payment_sms
|
||||
return send_payment_sms(job, provider_factory=get_sms_provider)
|
||||
issued = None
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-10-11 11:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0018_alter_smstemplate_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='smstemplate',
|
||||
name='event_type',
|
||||
field=models.CharField(choices=[('CUSTOMER_TICKET_CREATED', 'ثبت تیکت توسط مشتری'), ('STAFF_TICKET_CREATED', 'ثبت تیکت توسط کارشناس'), ('STAFF_REPLY', 'پاسخ کارشناس'), ('PAYMENT_NOTICE_CREATED', 'صدور اعلامیه واریز'), ('PAYMENT_RECEIPT_APPROVED', 'تأیید نهایی فیش واریز'), ('PAYMENT_RECEIPT_REJECTED', 'رد فیش واریز'), ('UNIT_SUPERVISOR_NEW_TICKET', 'اطلاع تیکت جدید به سرپرستان واحد')], max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def seed_rejection_template(apps, schema_editor):
|
||||
template = apps.get_model('notifications', 'SMSTemplate')
|
||||
template.objects.using(schema_editor.connection.alias).get_or_create(
|
||||
unit=None, event_type='PAYMENT_RECEIPT_REJECTED', defaults={
|
||||
'title': 'رد فیش واریز', 'is_active': True,
|
||||
'body': 'فیش واریزی {receipt_number} به مبلغ {amount_rial} ریال رد شد.\nدلیل کارشناس: {rejection_reason}\nاصلاح و ارسال دوباره فیش: {payment_link}\n{company_name}',
|
||||
'allowed_tags': ['recipient_name', 'company_name', 'notice_number', 'receipt_number',
|
||||
'amount_rial', 'invoice_number', 'payment_link', 'rejection_reason'],
|
||||
})
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [('notifications', '0019_alter_smstemplate_event_type')]
|
||||
operations = [migrations.RunPython(seed_rejection_template, migrations.RunPython.noop)]
|
||||
@@ -15,6 +15,7 @@ class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
STAFF_REPLY="STAFF_REPLY","پاسخ کارشناس"
|
||||
PAYMENT_NOTICE_CREATED="PAYMENT_NOTICE_CREATED","صدور اعلامیه واریز"
|
||||
PAYMENT_RECEIPT_APPROVED="PAYMENT_RECEIPT_APPROVED","تأیید نهایی فیش واریز"
|
||||
PAYMENT_RECEIPT_REJECTED="PAYMENT_RECEIPT_REJECTED","رد فیش واریز"
|
||||
UNIT_SUPERVISOR_NEW_TICKET="UNIT_SUPERVISOR_NEW_TICKET","اطلاع تیکت جدید به سرپرستان واحد"
|
||||
unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.CASCADE,null=True,blank=True,related_name="sms_templates",help_text="خالی یعنی الگوی عمومی و fallback.")
|
||||
event_type=models.CharField(max_length=50,choices=Event.choices)
|
||||
@@ -28,7 +29,7 @@ class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
def __str__(self): return self.get_event_type_display()
|
||||
@classmethod
|
||||
def tags_for_event(cls, event_type):
|
||||
if event_type in {'PAYMENT_NOTICE_CREATED', 'PAYMENT_RECEIPT_APPROVED'}:
|
||||
if event_type in {'PAYMENT_NOTICE_CREATED', 'PAYMENT_RECEIPT_APPROVED', 'PAYMENT_RECEIPT_REJECTED'}:
|
||||
from apps.payments.notifications import TAGS
|
||||
return list(TAGS)
|
||||
tags = sms_template_tags()
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.utils import timezone
|
||||
from apps.accounts.models import User
|
||||
from . import validators
|
||||
from .models import BankInstitution, CompanyBankAccount, PaymentNotice
|
||||
from .identity import customer_name, depositor_name
|
||||
from .deposit_identifier import generate_id_algorithm
|
||||
|
||||
|
||||
class ReceiptAlertConfigurationForm(forms.ModelForm):
|
||||
@@ -94,6 +96,7 @@ class NoticeForm(forms.Form):
|
||||
|
||||
|
||||
class ReceiptForm(forms.Form):
|
||||
depositor_name = forms.CharField(label='نام واریزکننده', max_length=150)
|
||||
receipt_mode = forms.ChoiceField(label='نوع واریز', choices=[('linked', 'واریز مرتبط (پیشنهادی)'), ('independent', 'واریز مستقل')], widget=forms.RadioSelect, initial='linked')
|
||||
notice_id = forms.ModelChoiceField(label='اعلامیه واریز', queryset=PaymentNotice.objects.none(), required=False)
|
||||
paid_amount = forms.CharField(label='مبلغ پرداختشده (ریال)')
|
||||
@@ -111,6 +114,7 @@ class ReceiptForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, customer, notice_ids=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial.setdefault('depositor_name', customer_name(customer))
|
||||
notices = PaymentNotice.objects.filter(customer=customer, cancelled_at__isnull=True)
|
||||
if notice_ids is not None:
|
||||
notices = notices.filter(pk__in=notice_ids)
|
||||
@@ -118,6 +122,9 @@ class ReceiptForm(forms.Form):
|
||||
self.fields['notice_id'].label_from_instance = lambda n: f'{n.subject} — {n.amount:,} ریال — {n.invoice_number}'
|
||||
accounts = Q(is_active=True, available_to_customers=True) | Q(is_active=True, paymentnotice__in=notices)
|
||||
self.fields['destination_account'].queryset = CompanyBankAccount.objects.filter(accounts).distinct()
|
||||
self.account_warnings = {str(a.pk): {'required': a.requires_deposit_id, 'bank': a.bank.name}
|
||||
for a in self.fields['destination_account'].queryset.select_related('bank')}
|
||||
self.customer_deposit_id = generate_id_algorithm(customer.local_mobile) if customer.local_mobile else ''
|
||||
current = jdatetime.date.fromgregorian(date=timezone.localdate())
|
||||
self.fields['paid_year'].choices = [('', 'سال')] + [(str(y), str(y)) for y in range(current.year, current.year - 11, -1)]
|
||||
self.initial.setdefault('submission_key', uuid.uuid4())
|
||||
@@ -127,6 +134,9 @@ class ReceiptForm(forms.Form):
|
||||
def clean_paid_amount(self):
|
||||
return validators.amount(self.cleaned_data['paid_amount'])
|
||||
|
||||
def clean_depositor_name(self):
|
||||
return depositor_name(self.cleaned_data['depositor_name'])
|
||||
|
||||
def clean_receipt_image(self):
|
||||
upload = self.cleaned_data['receipt_image']
|
||||
self.image_metadata = validators.image(upload)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Customer identity and the payer recorded on each receipt are independent."""
|
||||
from django.core.exceptions import ValidationError
|
||||
from apps.core.persian_text import normalize_persian_text
|
||||
|
||||
|
||||
def customer_name(customer):
|
||||
profile = getattr(customer, 'customer_profile', None)
|
||||
name = f'{profile.first_name} {profile.last_name}'.strip() if profile else ''
|
||||
return normalize_persian_text(name or customer.get_full_name())
|
||||
|
||||
|
||||
def depositor_name(value):
|
||||
name = normalize_persian_text(value)
|
||||
if not name.replace('\u200c', '').replace('\u200b', '').strip():
|
||||
raise ValidationError('نام واریزکننده را وارد کنید.')
|
||||
if len(name) > 150:
|
||||
raise ValidationError('نام واریزکننده باید حداکثر ۱۵۰ کاراکتر باشد.')
|
||||
return name
|
||||
|
||||
|
||||
def receipt_identity(customer, payer):
|
||||
name = customer_name(customer)
|
||||
payer = normalize_persian_text(payer)
|
||||
if not name:
|
||||
return payer or 'نام ثبت نشده'
|
||||
if not payer or name == payer:
|
||||
return name
|
||||
return f'{name} (واریزکننده: {payer})'
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [('payments', '0007_receipt_alerts')]
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='paymentreceiptrevision', name='depositor_name',
|
||||
field=models.CharField('نام واریزکننده', max_length=150, default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -134,6 +134,7 @@ def receipt_path(instance, filename):
|
||||
class PaymentReceiptRevision(models.Model):
|
||||
receipt = models.ForeignKey(PaymentReceipt, on_delete=models.PROTECT, related_name='revisions')
|
||||
version = models.PositiveIntegerField()
|
||||
depositor_name = models.CharField('نام واریزکننده', max_length=150)
|
||||
amount = models.BigIntegerField()
|
||||
paid_date = models.DateField()
|
||||
destination = models.ForeignKey(CompanyBankAccount, on_delete=models.PROTECT)
|
||||
|
||||
@@ -5,8 +5,8 @@ from django.utils import timezone
|
||||
from apps.notifications.services import enqueue
|
||||
from .models import PaymentNotification, PaymentNotificationAttempt
|
||||
|
||||
EVENTS = {'PAYMENT_NOTICE_CREATED', 'PAYMENT_RECEIPT_APPROVED'}
|
||||
TAGS = ['recipient_name', 'company_name', 'notice_number', 'receipt_number', 'amount_rial', 'invoice_number', 'payment_link']
|
||||
EVENTS = {'PAYMENT_NOTICE_CREATED', 'PAYMENT_RECEIPT_APPROVED', 'PAYMENT_RECEIPT_REJECTED'}
|
||||
TAGS = ['recipient_name', 'company_name', 'notice_number', 'receipt_number', 'amount_rial', 'invoice_number', 'payment_link', 'rejection_reason']
|
||||
|
||||
|
||||
def notice_sms_pending(notice):
|
||||
@@ -17,7 +17,7 @@ def context(customer, notice=None, receipt=None, amount=0):
|
||||
profile = getattr(customer, 'customer_profile', None)
|
||||
return {'recipient_name': str(profile) if profile else customer.username, 'company_name': settings.COMPANY_NAME,
|
||||
'notice_number': notice.number if notice else '', 'receipt_number': receipt.number if receipt else '',
|
||||
'amount_rial': f'{amount:,}', 'invoice_number': notice.invoice_number if notice else '',
|
||||
'amount_rial': f'{amount:,}', 'invoice_number': notice.invoice_number if notice else '', 'rejection_reason': '',
|
||||
'payment_link': settings.PUBLIC_BASE_URL + (reverse('payments:receipt_detail', args=[receipt.public_id]) if receipt else reverse('payments:notices') + f'?notice={notice.public_id}')}
|
||||
|
||||
|
||||
@@ -31,9 +31,25 @@ def queue_approval(receipt, cycle):
|
||||
PaymentNotification.objects.get_or_create(job=job, defaults={'cycle': cycle, 'unit': receipt.owning_unit, 'context': context(receipt.customer, notice=receipt.notice, receipt=receipt, amount=cycle.revision.amount)})
|
||||
|
||||
|
||||
def queue_rejection(receipt, cycle, reason):
|
||||
job = enqueue('SMS', 'PAYMENT_RECEIPT_REJECTED', None, f'payment-rejected:{cycle.pk}', customer=receipt.customer, staff_user=cycle.finalized_by)
|
||||
details = context(receipt.customer, notice=receipt.notice, receipt=receipt, amount=cycle.revision.amount)
|
||||
PaymentNotification.objects.get_or_create(job=job, defaults={'cycle': cycle, 'unit': receipt.owning_unit,
|
||||
'context': {**details, 'rejection_reason': reason}})
|
||||
|
||||
|
||||
def payment_sms_body(job, notification):
|
||||
"""Editable templates must always include the customer's rejection reason."""
|
||||
from apps.notifications.management.commands.process_notifications import _render_sms
|
||||
body = _render_sms(job.event_type, notification.context, notification.unit)
|
||||
reason = notification.context.get('rejection_reason', '')
|
||||
if reason and reason not in body:
|
||||
body += '\nدلیل کارشناس: ' + reason
|
||||
return body
|
||||
|
||||
|
||||
def send_payment_sms(job, provider=None, provider_factory=None):
|
||||
from apps.notifications.models import SMSTemplate
|
||||
from apps.notifications.management.commands.process_notifications import _render_sms
|
||||
notification = PaymentNotification.objects.get(job=job)
|
||||
# Persist the attempt before rendering/provider construction: failed preparation is history too.
|
||||
attempt = PaymentNotificationAttempt.objects.create(notification=notification)
|
||||
@@ -52,7 +68,7 @@ def send_payment_sms(job, provider=None, provider_factory=None):
|
||||
notification.context = {**notification.context, 'payment_link': url}
|
||||
notification.save(update_fields=['context', 'access_link', 'rendered_body'])
|
||||
if not notification.rendered_body:
|
||||
body = _render_sms(job.event_type, notification.context, notification.unit)
|
||||
body = payment_sms_body(job, notification)
|
||||
template = SMSTemplate.objects.filter(event_type=job.event_type, is_active=True, unit=notification.unit).first() if notification.unit_id else None
|
||||
template = template or SMSTemplate.objects.get(event_type=job.event_type, is_active=True, unit__isnull=True)
|
||||
notification.rendered_body = body
|
||||
|
||||
@@ -17,10 +17,10 @@ RECEIPT_ALERT_EVENT = 'PAYMENT_RECEIPT_RECEIVED'
|
||||
|
||||
|
||||
def receipt_alert_context(revision):
|
||||
from .identity import receipt_identity
|
||||
customer = revision.receipt.customer
|
||||
profile = getattr(customer, 'customer_profile', None)
|
||||
name = f'{profile.first_name} {profile.last_name}'.strip() if profile else ''
|
||||
return {'customer_name': name or customer.get_full_name() or 'نام ثبت نشده',
|
||||
return {'customer_name': receipt_identity(customer, revision.depositor_name),
|
||||
'depositor_name': revision.depositor_name,
|
||||
'customer_mobile': customer.local_mobile, 'amount_rial': f'{revision.amount:,}',
|
||||
'received_at': jalali_datetime(revision.created_at), 'paid_date': jalali_date(revision.paid_date),
|
||||
'receipt_number': revision.receipt.number, 'revision_version': str(revision.version), 'manager_name': '',
|
||||
@@ -83,6 +83,8 @@ def send_receipt_alert(job, sms_provider_factory=None):
|
||||
if not alert.rendered_body:
|
||||
validate_alert_template(alert.message_template)
|
||||
alert.rendered_body = alert.message_template.format(**alert.context)
|
||||
if alert.context.get('depositor_name') and '{customer_name}' not in alert.message_template:
|
||||
alert.rendered_body += '\nمشتری / واریزکننده: ' + alert.context['customer_name']
|
||||
alert.save(update_fields=['rendered_body'])
|
||||
if job.channel == 'SMS':
|
||||
from apps.integrations.sms.providers import get_sms_provider
|
||||
|
||||
@@ -44,6 +44,7 @@ def notices_with_totals(queryset):
|
||||
def receipts_with_current_values(queryset):
|
||||
current = PaymentReceiptRevision.objects.filter(receipt_id=OuterRef('pk'), version=OuterRef('current_version'))
|
||||
return queryset.annotate(current_amount=Subquery(current.values('amount')[:1]),
|
||||
current_depositor_name=Subquery(current.values('depositor_name')[:1]),
|
||||
current_paid_date=Subquery(current.values('paid_date')[:1]))
|
||||
|
||||
|
||||
|
||||
@@ -143,7 +143,8 @@ def assign_cycle(revision, actor):
|
||||
|
||||
def receipt_metadata(data):
|
||||
from .deposit_identifier import normalize_deposit_identifier
|
||||
return {'amount': data['paid_amount'], 'paid_date': data['paid_date'], 'destination': data['destination_account'],
|
||||
from .identity import depositor_name
|
||||
return {'depositor_name': depositor_name(data.get('depositor_name')), 'amount': data['paid_amount'], 'paid_date': data['paid_date'], 'destination': data['destination_account'],
|
||||
'origin_bank': data.get('origin_bank'), 'origin_other': data.get('origin_other', ''),
|
||||
'deposit_id': normalize_deposit_identifier(data.get('deposit_id', '')), 'tracking_number': data.get('tracking_number', ''),
|
||||
'description': data.get('description', '')}
|
||||
@@ -157,6 +158,7 @@ def validate_destination(account, notice):
|
||||
|
||||
|
||||
def _submit_receipt(actor, customer, data, file_info, created_files):
|
||||
fields = receipt_metadata(data)
|
||||
# Same-customer lock serializes retries of a submission nonce before inserting it.
|
||||
User.objects.select_for_update().get(pk=customer.pk)
|
||||
existing = PaymentReceipt.objects.filter(submission_key=data['submission_key']).first()
|
||||
@@ -166,7 +168,8 @@ def _submit_receipt(actor, customer, data, file_info, created_files):
|
||||
raise PermissionDenied
|
||||
if (revision.amount != data['paid_amount'] or revision.paid_date != data['paid_date'] or
|
||||
revision.destination_id != data['destination_account'].pk or revision.sha256 != file_info['sha256'] or
|
||||
existing.notice_id != getattr(data.get('notice_id'), 'pk', None)):
|
||||
existing.notice_id != getattr(data.get('notice_id'), 'pk', None) or
|
||||
revision.depositor_name != fields['depositor_name']):
|
||||
raise ValidationError('این ارسال قبلاً ثبت شده است؛ برای اطلاعات متفاوت فرم تازه باز کنید.')
|
||||
return existing
|
||||
notice = data.get('notice_id')
|
||||
@@ -178,7 +181,6 @@ def _submit_receipt(actor, customer, data, file_info, created_files):
|
||||
validate_destination(account, notice)
|
||||
receipt = PaymentReceipt.objects.create(customer=customer, submitted_by=actor, notice=notice,
|
||||
owning_unit=notice.owning_unit if notice else (TicketPolicy.primary_unit(actor) if policies.is_staff(actor) else None), submission_key=data['submission_key'])
|
||||
fields = receipt_metadata(data)
|
||||
fields['destination'] = account
|
||||
fields['deposit_id'] = fields['deposit_id'] or (notice.deposit_id if notice and notice.account_id == account.pk else '')
|
||||
revision = PaymentReceiptRevision(receipt=receipt, version=1,
|
||||
@@ -226,7 +228,7 @@ def submit_receipt(actor, customer, data, *, access_link=None):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def decide_receipt(actor, receipt_id, version, action, reason=''):
|
||||
def decide_receipt(actor, receipt_id, version, action, reason='', *, rejection_confirmed=True):
|
||||
# Lock only the receipt: PostgreSQL cannot lock the nullable side of a LEFT JOIN.
|
||||
receipt = PaymentReceipt.objects.select_for_update(of=('self',)).select_related('owning_unit').get(public_id=receipt_id)
|
||||
# Never authorize a financial vote using a stale User/profile/permission cache.
|
||||
@@ -244,6 +246,8 @@ def decide_receipt(actor, receipt_id, version, action, reason=''):
|
||||
allowed = policies.can_decide(actor, receipt, cycle, senior=True)
|
||||
if not allowed:
|
||||
raise PermissionDenied
|
||||
if action == 'REJECT' and not rejection_confirmed:
|
||||
raise ValidationError('برای رد فیش، ارسال دلیل رد به مشتری را تأیید کنید.')
|
||||
reason = reason.strip()
|
||||
if action in {'REJECT', 'SENIOR_APPROVE'} and not reason:
|
||||
raise ValidationError('دلیل این تصمیم را بنویسید.')
|
||||
@@ -277,6 +281,9 @@ def decide_receipt(actor, receipt_id, version, action, reason=''):
|
||||
if status == 'APPROVED':
|
||||
from .notifications import queue_approval
|
||||
queue_approval(receipt, cycle)
|
||||
elif status == 'REJECTED':
|
||||
from .notifications import queue_rejection
|
||||
queue_rejection(receipt, cycle, reason)
|
||||
return receipt
|
||||
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ def receipt_form_page(request, customer, staff=False):
|
||||
image_policy = AttachmentPolicy.objects.filter(attachment_type='IMAGE', is_enabled=True).first()
|
||||
ctx = {'form': form, 'selected_notice': selected, 'notice_data': notice_data, 'financial_page': True,
|
||||
'max_bytes': image_policy.max_file_size if image_policy else 0, 'staff_form': staff, 'customer': customer,
|
||||
'account_requirements': {str(a.pk): a.requires_deposit_id for a in form.fields['destination_account'].queryset}}
|
||||
'account_requirements': form.account_warnings, 'customer_deposit_id': form.customer_deposit_id}
|
||||
return render(request, 'payments/receipt_form.html', staff_context(**ctx) if staff else access.customer_context(request, **ctx))
|
||||
|
||||
|
||||
@@ -221,7 +221,8 @@ def decision(request, public_id):
|
||||
get_object_or_404(policies.visible(PaymentReceipt.objects.all(),request.user),public_id=public_id)
|
||||
try:
|
||||
version = int(request.POST.get('version', '0'))
|
||||
receipt = services.decide_receipt(request.user, public_id, version, request.POST.get('action'), request.POST.get('reason', ''))
|
||||
receipt = services.decide_receipt(request.user, public_id, version, request.POST.get('action'), request.POST.get('reason', ''),
|
||||
rejection_confirmed=request.POST.get('confirm_rejection') == '1')
|
||||
messages.success(request, 'تصمیم ثبت شد. ' + receipt.get_status_display())
|
||||
except (ValidationError, ValueError) as error:
|
||||
messages.error(request, '؛ '.join(error.messages) if isinstance(error, ValidationError) else 'نسخه فیش معتبر نیست.')
|
||||
@@ -505,8 +506,10 @@ def receipt_revise(request, public_id):
|
||||
receipt=get_object_or_404(access.visible_payments(request, PaymentReceipt.objects.all()),public_id=public_id,customer=request.payment_customer,status='REJECTED')
|
||||
import jdatetime
|
||||
current = receipt.revisions.get(version=receipt.current_version)
|
||||
from .identity import customer_name
|
||||
paid = jdatetime.date.fromgregorian(date=current.paid_date)
|
||||
initial={'receipt_mode':'linked' if receipt.notice_id else 'independent','notice_id':receipt.notice_id,
|
||||
'depositor_name':current.depositor_name or customer_name(request.payment_customer),
|
||||
'paid_amount':str(current.amount),'destination_account':current.destination_id,'deposit_id':current.deposit_id,
|
||||
'paid_day':str(paid.day),'paid_month':str(paid.month),'paid_year':str(paid.year),
|
||||
'origin_bank':current.origin_bank_id,'origin_other':current.origin_other,
|
||||
@@ -529,5 +532,5 @@ def receipt_revise(request, public_id):
|
||||
notice_data=[selectors.notice_form_data(n) for n in selectors.recent_pending_notice_choices(form, retain_original=receipt.notice_id)]
|
||||
return render(request,'payments/receipt_form.html',{'form':form,'notice_data':notice_data,'max_bytes':policy.max_file_size if policy else 0,
|
||||
'payment_link_access': link,
|
||||
'account_requirements':{str(a.pk):a.requires_deposit_id for a in form.fields['destination_account'].queryset},
|
||||
'account_requirements':form.account_warnings, 'customer_deposit_id':form.customer_deposit_id,
|
||||
'financial_page':True,'revision_version':receipt.current_version})
|
||||
@@ -20,7 +20,15 @@
|
||||
|
||||
## متن و سابقه
|
||||
|
||||
تگهای متن: `{amount_rial}` (ریال)، `{customer_name}`، `{customer_mobile}`، `{received_at}` (تاریخ و ساعت شمسی)، `{paid_date}` (تاریخ واریز شمسی)، `{receipt_number}`، `{revision_version}`، `{receipt_link}` و `{manager_name}`. نام خالی مشتری با «نام ثبت نشده» جایگزین میشود. قالببندی اضافی یا دسترسی به ویژگیهای شیء در تگها پذیرفته نمیشود.
|
||||
تگهای متن: `{amount_rial}` (ریال)، `{customer_name}`، `{customer_mobile}`، `{received_at}` (تاریخ و ساعت شمسی)، `{paid_date}` (تاریخ واریز شمسی)، `{receipt_number}`، `{revision_version}`، `{receipt_link}` و `{manager_name}`. تگ `{customer_name}` شامل نام مشتری و نام واریزکننده ثبتشده روی نسخه فیش است؛ اگر این دو نام یکسان باشند، فقط یک بار نوشته میشود. اگر نام مشتری خالی باشد، نام واریزکننده نمایش داده میشود. در قالب سفارشی بدون این تگ نیز هویت مشتری/واریزکننده به متن اضافه میشود. نامها با قواعد یکسانسازی فارسی مقایسه میشوند. قالببندی اضافی یا دسترسی به ویژگیهای شیء در تگها پذیرفته نمیشود.
|
||||
|
||||
## نام واریزکننده و رد فیش
|
||||
|
||||
فیلد «نام واریزکننده» در تمام مسیرهای ثبت و اصلاح فیش اجباری است. نام پروفایل مشتری (و در نبود آن، نام حساب کاربری) پیشفرض فرم است و قابل تغییر میماند؛ ثبت این نام، پروفایل مشتری را تغییر نمیدهد. هر نسخه نام مستقل دارد. migration `payments.0008_paymentreceiptrevision_depositor_name` برای نسخههای قدیمی مقدار خالی میگذارد، چون نام واقعی واریزکننده قابل بازسازی نیست؛ پنل این وضعیت را مشخص میکند و هنگام اصلاح، ثبت نام الزامی است.
|
||||
|
||||
پس از رد فیش توسط تأییدکننده مجاز، یک پیامک مشتری با رویداد `PAYMENT_RECEIPT_REJECTED` در صف موجود ثبت میشود. پیش از ثبت، صفحه بررسی هشدار میدهد که دلیل رد برای مشتری ارسال میشود و تأیید کارشناس را با نمایش متن دلیل میگیرد. درخواست HTTP بدون تأیید پذیرفته نمیشود. پیامک شامل دلیل کامل و لینک کوتاه مشاهده و اصلاح فیش است؛ کلید یکتای چرخه بررسی جلوی ساخت پیامک تکراری را میگیرد. ثبت تصمیم و صف در یک تراکنش انجام میشود و ارسال شبکهای در Worker است.
|
||||
|
||||
migrationهای `notifications.0019` و `notifications.0020` رویداد و قالب عمومی قابل ویرایش آن را اضافه میکنند. تگ `{rejection_reason}` متن دلیل است؛ اگر قالب سفارشی آن را حذف کند، دلیل همچنان به انتهای پیام اضافه میشود. پس از انتشار، migrationها را اجرا و Worker موجود را بازراهاندازی کنید.
|
||||
|
||||
بله فایل خصوصی را مستقیماً همراه متن دریافت میکند؛ آدرس عمومی تصویر ایجاد نمیشود. لینک پیشفرض متن به جزئیات محافظتشده کارشناسان است. PNG/JPEG بهصورت تصویر و دیگر فرمتهای مجاز فیش بهصورت فایل تصویری ارسال میشوند.
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# ارزیابی نام واریزکننده و پیامک رد فیش
|
||||
|
||||
تاریخ ارزیابی: ۱۹ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۱۱)
|
||||
|
||||
## رفتار پیادهشده
|
||||
|
||||
- تمام فرمهای ثبت و اصلاح فیش، نام واریزکننده اجباری و قابل ویرایش دارند. نام از پروفایل مشتری یا نام حساب پر میشود؛ POST بدون نام حتی با وجود نام در پروفایل رد میشود.
|
||||
- نام روی نسخه فیش ذخیره میشود و اصلاح آن، نام نسخه قبلی و پروفایل مشتری را تغییر نمیدهد. نام مشتری و نام واریزکننده در میزکار و جزئیات کارکنان نمایش داده میشوند.
|
||||
- اعلان ثبت فیش در پیامک مدیران و بله هر دو نام متفاوت را نمایش میدهد؛ نام یکسان پس از یکسانسازی حروف و فاصلهها فقط یک بار میآید. اطلاعات اعلان snapshot زمان ثبت نسخه است.
|
||||
- رد فیش با دلیل و تأیید صریح کارشناس انجام میشود. انصراف از دیالوگ، فرم و دکمهها را قابل استفاده نگه میدارد. کنترل مجوز پیش از کنترل تأیید انجام میشود.
|
||||
- پیامک رد در تراکنش تصمیم و با کلید یکتای چرخه بررسی ساخته میشود؛ ارسال از Worker موجود انجام میشود. دلیل کامل حتی در قالب سفارشی بدون تگ دلیل به مشتری میرسد.
|
||||
- هشدار قرمز بانک شناسهدار، نام بانک و شناسه اختصاصی تولیدشده با الگوریتم موجود را نمایش میدهد و با تغییر حساب مقصد بهروز میشود. الزام واردکردن شناسه در خود عملیات بانکی توضیح داده میشود؛ فیلد اختیاری شناسه درجشده روی فیش مطابق قرارداد فعلی حفظ شده است.
|
||||
|
||||
## نتایج بررسی
|
||||
|
||||
- کل تستهای سمت سرور: ۵۱۴ موفق، ۷ ردشده از اجرا به علت نیاز به PostgreSQL؛ محیط فعلی SQLite است.
|
||||
- تستهای مرورگری پرداخت: ۴۱ سناریوی متمایز موفق، شامل تست نسخهگذاری PWA. یک خطای واقعی قفل شدن دکمه پس از انصراف کشف، اصلاح و دوباره تست شد.
|
||||
- ۱۶ تست جدید سمت سرور و ۶ سناریوی جدید مرورگری برای نام، هشدار بانک و تأیید رد اضافه شدند.
|
||||
- تستهای ارسال، درگاه پیامک و بله را mock میکنند و پیامی به مشتری یا گروه واقعی نمیفرستند.
|
||||
- `manage.py check` بدون اشکال؛ `makemigrations --check --dry-run` بدون تغییر جامانده؛ بررسی whitespace فایلهای این تغییر بدون خطا.
|
||||
- `PROJECT_TREE.txt`، تستهای موجود و مستندات اعلان بررسی شدند. Graphify برای ارتباط مستندات و بخشها و Ripwire برای impact، uses، قرارداد و دروازه تست به کار رفتند.
|
||||
- Ripwire پس از بازخوانی بدون کش یک هشدار عمده باقیمانده دارد: افزایش تعداد پارامترهای `decide_receipt` به دلیل پارامتر اختیاری تأیید رد. این تغییر عمدی است؛ بررسی قرارداد هیچ فراخوان ناسازگاری گزارش نکرد و تستهای مجوز و تصمیم موفق بودند. یافتههای عدم پوشش AST، جایگزین نتایج اجرای تست نیستند.
|
||||
- محتوای واقعی `/chaharsu/payment-receipt/` و `/staff/payments/` و فایلهای JavaScript سروشده روی سرور محلی بررسی شدند. دقیقاً یک شنونده روی `0.0.0.0:8000` وجود دارد. UUIDهای نمونه درخواست در دیتابیس محلی نبودند؛ صفحات جزئیات با داده مستقل در live_server تست شدند.
|
||||
|
||||
## انتشار
|
||||
|
||||
روی دیتابیس توسعه، پس از backup، migrationهای `payments.0008`، `notifications.0019` و `notifications.0020` اجرا شدند. برای محیط اصلی نیز migrationها باید اجرا و Worker `process_notifications` با کد جدید بازراهاندازی شود. migrationهای قبلی و تغییرات موجود کاربر حفظ شدند. برای فیشهای قدیمی نام واقعی واریزکننده جعل یا از پروفایل بازسازی نمیشود؛ وضعیت ثبت نشدن نام در پنل مشخص است.
|
||||
@@ -121,3 +121,5 @@
|
||||
@media(max-width:639px){.pay-identifier-control{flex-direction:column;gap:8px}.pay-identifier-control>.pay-button{align-self:stretch}}
|
||||
.pay-pagination{flex-wrap:wrap}.pay-pagination small{flex-basis:100%;text-align:center;color:#526c86}.pay-sms-panel{overflow-wrap:anywhere}.pay-sms-panel>form>.pay-button{width:100%;white-space:normal;line-height:1.8}.pay-sms-panel button:disabled{opacity:.6;cursor:not-allowed}.pay-sms-record{padding:16px 0;border-top:1px solid #e7edf2}.pay-sms-record header{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap}.pay-sms-record p{margin:8px 0;font-size:13px;line-height:1.9}.pay-sms-record time{font-size:12px;color:#526c86}.pay-sms-panel .pay-status{white-space:normal;max-width:100%;line-height:1.8}.pay-sms-record summary{cursor:pointer;font-size:13px;padding:10px 0;color:#17435d}.pay-sms-body{font-size:13px;line-height:1.9;background:#f0f8fc;padding:12px;border-radius:10px}.pay-sms-attempt{padding:10px 0;border-top:1px dashed #dce5ed}.pay-sms-error{color:#9f1f1f}
|
||||
.receipt-upload{padding:18px;margin:16px 0;border:1px solid #c7dfeb;border-radius:16px;background:#f0f8fc;color:#17435d;overflow-wrap:anywhere}.receipt-upload[hidden]{display:none}.receipt-upload p{margin:8px 0;font-size:14px}.receipt-upload small{display:block;color:#526c86}.receipt-upload-meter{display:flex;align-items:center;gap:12px;margin-top:12px}.receipt-upload-meter progress{width:100%;flex:1;min-width:0;height:12px;accent-color:#087d8b;direction:ltr}.receipt-upload-meter span{min-width:44px;text-align:center;font-weight:700;font-variant-numeric:tabular-nums}.customer-receipt-actions button:disabled{opacity:.65;cursor:wait}.customer-receipt-form.is-uploading input,.customer-receipt-form.is-uploading select,.customer-receipt-form.is-uploading textarea{opacity:.75}
|
||||
|
||||
.chaharsu .pay-deposit-warning{color:#b42318;background:#fff4f2;border:1px solid #f5b5ad;border-radius:12px;padding:14px 16px;font-size:14px;line-height:2;overflow-wrap:anywhere}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Preview the exact rejection reason before submitting the staff decision. */
|
||||
(() => {
|
||||
const form = document.querySelector('.pay-decision-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', event => {
|
||||
const confirmation = form.elements.namedItem('confirm_rejection');
|
||||
confirmation.value = '';
|
||||
if (event.submitter?.value !== 'REJECT') return;
|
||||
const reason = form.elements.namedItem('reason');
|
||||
if (!reason.value.trim()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
reason.setCustomValidity('دلیل رد فیش را برای مشتری بنویسید.');
|
||||
reason.reportValidity();
|
||||
reason.setCustomValidity('');
|
||||
reason.focus();
|
||||
return;
|
||||
}
|
||||
if (!window.confirm('این فیش رد میشود و دلیل زیر با پیامک برای مشتری ارسال خواهد شد:\n\n' + reason.value.trim() + '\n\nرد فیش و ارسال این دلیل را تأیید میکنید؟')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
confirmation.value = '1';
|
||||
});
|
||||
})();
|
||||
@@ -5,6 +5,7 @@
|
||||
const form=document.querySelector('#receipt-form');if(!form)return;
|
||||
const q=name=>form.elements.namedItem(name),button=document.querySelector('#submit-receipt');
|
||||
const notices=JSON.parse(document.querySelector('#notice-data').textContent);
|
||||
const accounts=JSON.parse(document.querySelector('#account-requirements').textContent);
|
||||
const day=q('paid_day'),month=q('paid_month'),year=q('paid_year');
|
||||
const digits=value=>String(value).replace(/[۰-۹]/g,x=>String(x.charCodeAt(0)-1776)).replace(/[٠-٩]/g,x=>String(x.charCodeAt(0)-1632));
|
||||
const formatter=new Intl.DateTimeFormat('en-US',{calendar:'persian',numberingSystem:'latn',timeZone:'Asia/Tehran',year:'numeric',month:'numeric',day:'numeric'});
|
||||
@@ -24,6 +25,10 @@ function monthUpdate(){
|
||||
[month,year].forEach(control=>control.addEventListener('change',monthUpdate));day.addEventListener('change',dateUpdate);
|
||||
document.querySelector('#date-today').addEventListener('click',()=>{const p=parts(new Date());year.value=p.year;month.value=p.month;monthUpdate();day.value=p.day;dateUpdate();day.focus();});monthUpdate();
|
||||
function sync(){
|
||||
const destination=accounts[q('destination_account').value];
|
||||
const warning=document.querySelector('#destination-deposit-warning');
|
||||
warning.hidden=!destination?.required;
|
||||
warning.querySelector('[data-destination-bank]').textContent=destination?.bank || '';
|
||||
const linked=q('receipt_mode').value==='linked';const select=q('notice_id');form.querySelector('[data-field=notice_id]').hidden=!linked;select.disabled=!linked;select.required=linked;
|
||||
if(!linked)select.value='';
|
||||
const notice=linked?notices.find(n=>n.id===select.value):null;const summary=document.querySelector('#selected-payment');summary.hidden=!notice;
|
||||
@@ -55,6 +60,7 @@ let revision=0;form.addEventListener('input',()=>revision++);form.addEventListen
|
||||
form.addEventListener('submit',async event=>{
|
||||
event.preventDefault();if(busy)return;busy=true;button.disabled=true;button.textContent='در حال بررسی تصویر…';document.querySelector('#form-status').textContent='';const version=revision,errors=new Map();let navigating=false;
|
||||
try{
|
||||
if(!q('depositor_name').value.replace(/[\s\u200c\u200b]/g,''))errors.set('depositor_name','نام واریزکننده را وارد کنید.');
|
||||
const value=digits(q('paid_amount').value).trim();if(!/^\d+$/.test(value) && !/^\d{1,3}(?:[,٬ ]\d{3})+$/.test(value))errors.set('paid_amount','مبلغ را به ریال و با عدد صحیح وارد کنید.');else{const amount=BigInt(value.replace(/[,٬ ]/g,''));if(amount<=0n || amount>9007199254740991n)errors.set('paid_amount','مبلغ باید مثبت و در محدوده مجاز باشد.');}
|
||||
if(q('receipt_mode').value==='linked' && !q('notice_id').value)errors.set('notice_id','اعلامیه را انتخاب کنید یا واریز مستقل را بزنید.');
|
||||
if(!q('destination_account').value)errors.set('destination_account','حساب مقصد را انتخاب کنید.');
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
<div id="selected-payment" class="pay-supplement" hidden><h3 data-selected-notice-title></h3><p data-selected-notice-summary></p><p class="pay-deposit-warning" data-selected-deposit-warning hidden>هنگام واریز، حتماً شناسه واریز ارائهشده را در فرم بانکی، موبایلبانک یا اینترنتبانک وارد کنید؛ واریز بدون این شناسه معتبر نخواهد بود.</p><a class="pay-text-link" data-selected-notice-link>مشاهده اطلاعات و کپی شمارهها {% include 'payments/_icon.html' with name='arrow' %}</a></div>
|
||||
</section>
|
||||
<section class="pay-card" aria-labelledby="receipt-title">
|
||||
<h2 id="receipt-title">اطلاعات فیش پرداخت</h2><p class="pay-section-help">مبلغ، حساب مقصد، تاریخ کامل و تصویر فیش الزامیاند.</p>
|
||||
<h2 id="receipt-title">اطلاعات فیش پرداخت</h2><p class="pay-section-help">نام واریزکننده، مبلغ، حساب مقصد، تاریخ کامل و تصویر فیش الزامیاند.</p>
|
||||
<div class="customer-field-grid">
|
||||
{% include 'payments/_receipt_field.html' with field=form.depositor_name hint='نام شخصی که وجه را واریز کرده است؛ اگر واریزکننده شخص دیگری است، نام او را بنویسید.' %}
|
||||
{% include 'payments/_receipt_field.html' with field=form.paid_amount hint='مبلغی که واقعاً پرداخت کردهاید؛ ممکن است با مبلغ اعلامشده متفاوت باشد.' %}
|
||||
{% include 'payments/_receipt_field.html' with field=form.tracking_number hint='شماره درجشده روی فیش بانکی' %}
|
||||
{% include 'payments/_receipt_field.html' with field=form.origin_bank %}
|
||||
@@ -25,6 +26,7 @@
|
||||
{% include 'payments/_receipt_field.html' with field=form.origin_other %}
|
||||
{% include 'payments/_receipt_field.html' with field=form.deposit_id hint='در صورت تمایل، شناسهای را که هنگام پرداخت وارد کردهاید بنویسید؛ ارسال فیش بدون تکمیل این فیلد نیز امکانپذیر است.' %}
|
||||
</div>
|
||||
<p id="destination-deposit-warning" class="pay-deposit-warning" role="alert" hidden>واریز به بانک <strong data-destination-bank></strong> باید با ثبت شناسه واریز اختصاصی شما، <bdi>{{ customer_deposit_id }}</bdi>، در فرم بانکی، موبایلبانک یا اینترنتبانک انجام شده باشد. در صورت ثبت نکردن این شناسه، ممکن است مبلغ واریزی به حساب شما برگشت بخورد. پیش از ارسال فیش، شناسه ثبتشده روی آن را بررسی کنید.</p>
|
||||
<fieldset class="customer-date"><legend>تاریخ واریز <span class="pay-optional">شمسی · الزامی</span></legend><div class="customer-date-selects">{% include 'payments/_receipt_field.html' with field=form.paid_day %}{% include 'payments/_receipt_field.html' with field=form.paid_month %}{% include 'payments/_receipt_field.html' with field=form.paid_year %}</div><div class="customer-date-tools"><p class="pay-field-hint">تاریخ درجشده روی فیش را کامل انتخاب کنید.</p><button id="date-today" type="button" class="pay-today">امروز</button></div><p id="date-preview" aria-live="polite"></p><input id="paid-date-iso" type="hidden" name="paid_date" value=""></fieldset>
|
||||
<div class="pay-field customer-upload-field" data-field="receipt_image"><div class="customer-upload-zone">{% include 'payments/_icon.html' with name='upload' %}<label class="customer-file-button" for="{{ form.receipt_image.id_for_label }}">انتخاب تصویر فیش <span>الزامی</span></label>{{ form.receipt_image }}<p>تصویر JPG، PNG، GIF یا WEBP؛ حداکثر {{ max_bytes|filesizeformat }}.</p></div><p class="field-error" id="error-receipt_image" {% if not form.receipt_image.errors %}hidden{% endif %}>{% for error in form.receipt_image.errors %}{{ error }}{% endfor %}</p><div class="customer-file-selection" id="receipt-file-selection" hidden><p id="receipt-file-status" role="status" aria-live="polite"></p><button type="button" id="receipt-clear-file">حذف انتخاب</button></div><img id="receipt-file-preview" alt="پیشنمایش تصویر فیش" hidden></div>
|
||||
{% include 'payments/_receipt_field.html' with field=form.description %}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="pay-record-main">
|
||||
{% if tab == 'notices' %}<h3><a href="{% url 'staff_payments:notice_detail' item.public_id %}">{{ item.subject }}</a></h3><span class="pay-record-meta"><bdi>{{ item.number }}</bdi>{% if item.invoice_number %} · فاکتور {{ item.invoice_number }}{% endif %}</span>
|
||||
{% else %}<h3><a href="{% url 'staff_payments:receipt_detail' item.public_id %}"><bdi>{{ item.number }}</bdi></a></h3><span class="pay-record-meta">{% if item.notice %}مرتبط با {{ item.notice.subject }}{% else %}واریز مستقل{% endif %}</span>{% endif %}
|
||||
{% include 'payments/_customer_identity.html' with customer=item.customer %}
|
||||
{% include 'payments/_customer_identity.html' with customer=item.customer %}{% if tab != 'notices' %}<p class="pay-record-meta">نام واریزکننده: {{ item.current_depositor_name|default:'در فیش قدیمی ثبت نشده' }}</p>{% endif %}
|
||||
<div class="pay-record-review">{% if tab == 'notices' %}<span>ایجادکننده: {{ item.created_by.staff_profile.display_name|default:item.created_by.get_full_name|default:item.created_by.username }}</span>{% for linked in item.related_receipts %}{% if linked.status == 'APPROVED' %}{% for vote in linked.review_decisions %}<span>تأیید فیش {{ linked.number }}: {{ vote.actor_name }}</span>{% endfor %}{% endif %}{% endfor %}{% else %}{% if item.notice %}<span>ایجادکننده اعلامیه: {{ item.notice.created_by.staff_profile.display_name|default:item.notice.created_by.get_full_name|default:item.notice.created_by.username }}</span>{% endif %}{% if item.status == 'APPROVED' %}{% for vote in item.review_decisions %}<span>تأییدکننده: {{ vote.actor_name }}</span>{% endfor %}{% endif %}{% endif %}</div>
|
||||
</div>
|
||||
<div class="pay-record-amount"><strong>{% if tab == 'notices' %}{{ item.amount|intcomma:False }}{% else %}{{ item.current_amount|intcomma:False }}{% endif %} <small>ریال</small></strong><time>{% if tab == 'notices' %}{{ item.created_at|jalali_datetime }}{% else %}واریز {{ item.current_paid_date|jalali_date }}{% endif %}</time></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'payments/base.html' %}
|
||||
{% load jalali humanize %}
|
||||
{% load jalali humanize static %}
|
||||
{% block title %}فیش {{ receipt.number }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="chaharsu {% if staff_detail %}pay-workspace pay-detail-workspace{% else %}customer-finance customer-receipt-detail{% endif %}">
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="{% if staff_detail %}pay-detail-grid{% endif %}">
|
||||
<div>
|
||||
<article class="pay-card"><div class="pay-receipt-total"><span>مبلغ پرداختشده</span><h2>{{ revision.amount|intcomma:False }} <small>ریال</small></h2><p>تاریخ پرداخت: {{ revision.paid_date|jalali_date }}</p></div>
|
||||
<dl class="pay-facts"><div><dt>حساب مقصد</dt><dd>{{ revision.account_snapshot.name }}</dd></div><div><dt>بانک مقصد</dt><dd>{{ revision.account_snapshot.bank }}</dd></div><div><dt>صاحب حساب</dt><dd>{{ revision.account_snapshot.holder }}</dd></div><div><dt>شناسه واریز</dt><dd><bdi>{{ revision.deposit_id|default:'اعلام نشده' }}</bdi></dd></div>{% if revision.origin_bank %}<div><dt>بانک مبدأ</dt><dd>{{ revision.origin_bank.name }}{% if revision.origin_other %} · {{ revision.origin_other }}{% endif %}</dd></div>{% endif %}{% if revision.tracking_number %}<div><dt>شماره پیگیری</dt><dd><bdi>{{ revision.tracking_number }}</bdi></dd></div>{% endif %}</dl>
|
||||
<dl class="pay-facts"><div><dt>نام واریزکننده</dt><dd>{{ revision.depositor_name|default:"در فیش قدیمی ثبت نشده" }}</dd></div><div><dt>حساب مقصد</dt><dd>{{ revision.account_snapshot.name }}</dd></div><div><dt>بانک مقصد</dt><dd>{{ revision.account_snapshot.bank }}</dd></div><div><dt>صاحب حساب</dt><dd>{{ revision.account_snapshot.holder }}</dd></div><div><dt>شناسه واریز</dt><dd><bdi>{{ revision.deposit_id|default:'اعلام نشده' }}</bdi></dd></div>{% if revision.origin_bank %}<div><dt>بانک مبدأ</dt><dd>{{ revision.origin_bank.name }}{% if revision.origin_other %} · {{ revision.origin_other }}{% endif %}</dd></div>{% endif %}{% if revision.tracking_number %}<div><dt>شماره پیگیری</dt><dd><bdi>{{ revision.tracking_number }}</bdi></dd></div>{% endif %}</dl>
|
||||
{% if staff_detail %}<h3 class="pay-section-title">اطلاعات مقصد ثبتشده</h3><dl class="pay-facts">{% if revision.account_snapshot.account_number %}<div><dt>شماره حساب مقصد ثبتشده</dt><dd dir="ltr">{{ revision.account_snapshot.account_number }}</dd></div>{% endif %}{% if revision.account_snapshot.iban %}<div><dt>شبای مقصد ثبتشده</dt><dd dir="ltr">{{ revision.account_snapshot.iban }}</dd></div>{% endif %}{% if revision.account_snapshot.card_number %}<div><dt>کارت مقصد ثبتشده</dt><dd dir="ltr">{{ revision.account_snapshot.card_number }}</dd></div>{% endif %}</dl><p class="pay-section-help">شمارههای ثبتشده را با تصویر فیش تطبیق دهید.</p>{% endif %}
|
||||
<section class="pay-supplement">{% if receipt.notice %}<h3>اعلامیه مرتبط</h3><p>{% if staff_detail %}<a href="{% url 'staff_payments:notice_detail' receipt.notice.public_id %}">{{ receipt.notice.number }}</a>{% else %}<a href="{% url 'payments:notice_detail' receipt.notice.public_id %}">{{ receipt.notice.number }}</a>{% endif %} · مبلغ اعلامشده: {{ receipt.notice.amount|intcomma:False }} ریال{% if receipt.notice.invoice_number %} · فاکتور {{ receipt.notice.invoice_number }}{% endif %}</p>{% else %}<p>واریز مستقل</p>{% endif %}{% if revision.description %}<h3>توضیحات پرداخت</h3><p>{{ revision.description|linebreaksbr }}</p>{% endif %}</section>
|
||||
</article>
|
||||
@@ -20,10 +20,12 @@
|
||||
</div>
|
||||
<aside class="pay-detail-aside">
|
||||
{% if can_assign %}<form method="post" action="{% url 'staff_payments:assign_policy' receipt.public_id %}" class="pay-card">{% csrf_token %}<h2>تعیین تأییدکنندگان</h2><p class="pay-section-help">سیاست فعال تأیید را به این فیش اختصاص دهید.</p><button class="pay-button">اعمال سیاست فعال تأیید</button></form>{% endif %}
|
||||
{% if receipt.status == 'PENDING' or receipt.status == 'PARTIAL' %}{% if can_approve or can_senior %}<form class="pay-card pay-form pay-decision-form" method="post" action="{% url 'staff_payments:decision' receipt.public_id %}">{% csrf_token %}<input type="hidden" name="version" value="{{ revision.version }}"><h2>ثبت نتیجه بررسی</h2><p class="pay-section-help">پس از تطبیق مبلغ، حساب و تصویر فیش، تصمیم خود را ثبت کنید.</p><div class="pay-field"><label for="decision-reason">توضیحات و دلیل تأیید یا رد</label><textarea name="reason" id="decision-reason" aria-describedby="decision-help"></textarea><p id="decision-help" class="pay-field-hint">برای تأیید میتوانید توضیح بنویسید؛ برای رد، تأیید ارشد و مغایرت حساب، دلیل الزامی است.</p></div><p class="pay-inline-note">تأیید من تا پایان بررسیهای لازم، تأیید نهایی نیست.</p><div class="pay-decision-actions">{% if can_approve %}<button class="pay-button" name="action" value="APPROVE">ثبت تأیید من</button>{% endif %}{% if can_approve or can_senior %}<button class="pay-button pay-danger secondary" name="action" value="REJECT">رد فیش</button>{% endif %}{% if can_senior %}<button class="pay-button" name="action" value="SENIOR_APPROVE">تأیید نهایی ارشد</button>{% endif %}</div></form>{% endif %}{% endif %}
|
||||
{% if receipt.status == 'PENDING' or receipt.status == 'PARTIAL' %}{% if can_approve or can_senior %}<form class="pay-card pay-form pay-decision-form" method="post" action="{% url 'staff_payments:decision' receipt.public_id %}">{% csrf_token %}<input type="hidden" name="version" value="{{ revision.version }}"><h2>ثبت نتیجه بررسی</h2><p class="pay-section-help">پس از تطبیق مبلغ، حساب و تصویر فیش، تصمیم خود را ثبت کنید.</p><div class="pay-field"><label for="decision-reason">توضیحات و دلیل تأیید یا رد</label><textarea name="reason" id="decision-reason" aria-describedby="decision-help"></textarea><p id="decision-help" class="pay-field-hint">برای تأیید میتوانید توضیح بنویسید؛ برای رد، تأیید ارشد و مغایرت حساب، دلیل الزامی است. دلیل رد عیناً با پیامک برای مشتری ارسال میشود؛ لطفاً متن را با دقت و با لحنی مناسب مشتری بنویسید.</p></div><p class="pay-inline-note">تأیید من تا پایان بررسیهای لازم، تأیید نهایی نیست.</p><input type="hidden" name="confirm_rejection" value=""><noscript><label><input type="checkbox" name="confirm_rejection" value="1"> تأیید میکنم دلیل رد با پیامک برای مشتری ارسال شود.</label></noscript><div class="pay-decision-actions">{% if can_approve %}<button class="pay-button" name="action" value="APPROVE">ثبت تأیید من</button>{% endif %}{% if can_approve or can_senior %}<button class="pay-button pay-danger secondary" name="action" value="REJECT">رد فیش</button>{% endif %}{% if can_senior %}<button class="pay-button" name="action" value="SENIOR_APPROVE">تأیید نهایی ارشد</button>{% endif %}</div></form>{% endif %}{% endif %}
|
||||
{% if receipt.status == 'REJECTED' and not staff_detail %}<a class="pay-button" href="{% url 'payments:receipt_revise' receipt.public_id %}">اصلاح و ارسال دوباره فیش</a>{% endif %}
|
||||
<article class="pay-card"><h2>سوابق بررسی</h2><div class="pay-timeline">{% for vote in decisions %}<div class="pay-timeline-item"><strong>{{ vote.actor_name }}</strong><span>{{ vote.get_action_display }} · نسخه {{ vote.cycle.revision.version }}</span><time>{{ vote.created_at|jalali_datetime }}</time>{% if vote.reason %}<p>{{ vote.reason|linebreaksbr }}</p>{% else %}<p class="pay-field-hint">توضیحی ثبت نشده است.</p>{% endif %}</div>{% empty %}<p class="pay-section-help">هنوز تصمیمی ثبت نشده است.</p>{% endfor %}</div><h3 class="pay-section-title">نسخههای مدرک</h3>{% for item in versions %}<div class="pay-version-row"><a href="{% url 'payments:receipt_image' receipt.public_id %}?version={{ item.version }}">نسخه {{ item.version }}</a><div><time>{{ item.created_at|jalali_datetime }}</time><p class="pay-field-hint">{{ item.amount|intcomma:False }} ریال · واریز {{ item.paid_date|jalali_date }} · {{ item.cycle.get_status_display }}</p></div></div>{% endfor %}{% if revision_events %}<h3 class="pay-section-title">سوابق ارسال و اصلاح</h3>{% for event in revision_events %}<div class="pay-timeline-item"><strong>{{ event.actor_name }}</strong><span>{% if event.action == 'RECEIPT_REVISED' %}اصلاح و ارسال نسخه {{ event.details.version }}{% else %}ارسال فیش{% endif %}</span><time>{{ event.created_at|jalali_datetime }}</time></div>{% endfor %}{% endif %}{% if cycle.finalized_at %}<div class="pay-supplement"><h3>نهاییکننده</h3><p>{{ cycle.finalized_by.username }}</p><time>{{ cycle.finalized_at|jalali_datetime }}</time></div>{% endif %}</article>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}{{ block.super }}<script defer src="{% static 'js/chaharsu/receipt-decision.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .receipt_upload import ReceiptUploadPage
|
||||
|
||||
|
||||
class ReceiptCustomerDetailsPage(ReceiptUploadPage):
|
||||
def expect_payer(self, value):
|
||||
expect(self.page.get_by_label('نام واریزکننده')).to_have_value(value)
|
||||
|
||||
def enter_payer(self, name):
|
||||
self.page.get_by_label('نام واریزکننده').fill(name)
|
||||
|
||||
def expect_missing_payer(self):
|
||||
expect(self.page.locator('#error-depositor_name')).to_contain_text('نام واریزکننده را وارد کنید')
|
||||
|
||||
def select_destination(self, account_id, bank=None, identifier=None):
|
||||
self.page.locator('#id_destination_account').select_option(str(account_id))
|
||||
warning = self.page.locator('#destination-deposit-warning')
|
||||
if bank:
|
||||
expect(warning).to_be_visible()
|
||||
expect(warning).to_contain_text(bank)
|
||||
expect(warning).to_contain_text(identifier)
|
||||
expect(warning).to_contain_text('برگشت بخورد')
|
||||
assert warning.evaluate('el => getComputedStyle(el).color') == 'rgb(180, 35, 24)'
|
||||
else:
|
||||
expect(warning).to_be_hidden()
|
||||
|
||||
def reject(self, reason, *, confirm):
|
||||
expect(self.page.locator('#decision-help')).to_contain_text('با پیامک برای مشتری ارسال میشود')
|
||||
self.page.locator('#decision-reason').fill(reason)
|
||||
previews = []
|
||||
def answer(dialog):
|
||||
previews.append(dialog.message)
|
||||
dialog.accept() if confirm else dialog.dismiss()
|
||||
self.page.once('dialog', answer)
|
||||
self.page.get_by_role('button', name='رد فیش', exact=True).click()
|
||||
assert len(previews) == 1 and reason in previews[0] and 'برای مشتری ارسال' in previews[0]
|
||||
if confirm:
|
||||
expect(self.page.locator('.pay-status').first).to_have_text('رد شده')
|
||||
else:
|
||||
expect(self.page.get_by_role('button', name='رد فیش', exact=True)).to_be_visible()
|
||||
expect(self.page.locator('#decision-reason')).to_have_value(reason)
|
||||
@@ -21,7 +21,7 @@ def payment_case(settings,tmp_path):
|
||||
settings.MEDIA_ROOT=tmp_path
|
||||
bank,_=BankInstitution.objects.get_or_create(code='melli',defaults={'name':'ملی'})
|
||||
account=CompanyBankAccount.objects.create(name='حساب فروش شرکت',holder='شرکت',bank=bank,account_number='000123')
|
||||
customer=User.objects.create_user(username='payment-e2e-customer',mobile='09120009021',password=PASSWORD,account_type='CUSTOMER')
|
||||
customer=User.objects.create_user(username='payment-e2e-customer',mobile='09120009021',first_name='مشتری',last_name='آزمایشی',password=PASSWORD,account_type='CUSTOMER')
|
||||
reviewers=[]
|
||||
for index in range(2):
|
||||
user=User.objects.create_user(username=f'payment-e2e-reviewer-{index}',password=PASSWORD,account_type='STAFF')
|
||||
@@ -308,6 +308,7 @@ def test_customer_linked_receipt_status_and_reason_without_access_roster(live_se
|
||||
expect(page.get_by_role('button', name='ثبت تأیید من', exact=True)).to_be_visible()
|
||||
expect(page.get_by_role('heading', name='افراد تعیینشده', exact=True)).to_have_count(0)
|
||||
page.locator('#decision-reason').fill('تصویر فیش خوانا نیست')
|
||||
page.once('dialog', lambda dialog: dialog.accept())
|
||||
page.get_by_role('button', name='رد فیش', exact=True).click()
|
||||
expect(page.get_by_text('تصویر فیش خوانا نیست', exact=True)).to_be_visible()
|
||||
page.get_by_role('button', name='خروج', exact=True).first.click()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from apps.payments import services
|
||||
from apps.payments.access import issue_payment_link
|
||||
from apps.payments.deposit_identifier import generate_id_algorithm
|
||||
from apps.payments.models import CompanyBankAccount
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.receipt_customer_details import ReceiptCustomerDetailsPage
|
||||
from tests.e2e.test_payments_e2e import payment_case, PASSWORD, PNG
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode', ['independent', 'linked', 'staff', 'link'])
|
||||
def test_required_payer_on_every_receipt_entry_point(live_server, browser_page, payment_case, mode):
|
||||
customer, account, reviewers = payment_case
|
||||
customer.first_name = customer.last_name = ''
|
||||
customer.save()
|
||||
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': 'ثبت فیش با نام', 'amount': 12345})
|
||||
link_path = urlsplit(issue_payment_link(notice=notice)[1]).path if mode == 'link' else ''
|
||||
with browser_page() as page:
|
||||
page.set_viewport_size({'width': 320, 'height': 900})
|
||||
view = ReceiptCustomerDetailsPage(page, live_server.url)
|
||||
if mode == 'link':
|
||||
view.goto(link_path)
|
||||
page.get_by_role('link', name='ارسال فیش این واریز', exact=True).click()
|
||||
elif mode == 'staff':
|
||||
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
|
||||
view.goto(reverse('staff_payments:receipt_new', args=[customer.public_id]))
|
||||
else:
|
||||
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
|
||||
view.goto(reverse('payments:receipt_new') + ('?notice=' + str(notice.public_id) if mode == 'linked' else ''))
|
||||
if mode in ['independent', 'staff']:
|
||||
page.get_by_label('واریز مستقل', exact=True).check()
|
||||
view.expect_payer('')
|
||||
view.fill_receipt(account.pk, PNG)
|
||||
view.submit()
|
||||
view.expect_missing_payer()
|
||||
view.enter_payer('رضا احمدی')
|
||||
view.submit()
|
||||
expect(page.locator('.flash')).to_contain_text('فیش ثبت شد')
|
||||
expect(page.get_by_text('رضا احمدی', exact=True)).to_be_visible()
|
||||
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
|
||||
|
||||
|
||||
def test_identifier_warning_follows_destination_and_mode(live_server, browser_page, payment_case):
|
||||
customer, account, reviewers = payment_case
|
||||
account.requires_deposit_id = True
|
||||
account.save()
|
||||
other = CompanyBankAccount.objects.create(name='حساب بدون شناسه', holder='شرکت', bank=account.bank, account_number='1234')
|
||||
identifier = generate_id_algorithm(customer.local_mobile)
|
||||
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': 'شناسه اختصاصی', 'amount': 12345, 'deposit_id': identifier})
|
||||
with browser_page() as page:
|
||||
view = ReceiptCustomerDetailsPage(page, live_server.url)
|
||||
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
|
||||
view.goto(reverse('payments:receipt_new') + '?notice=' + str(notice.public_id))
|
||||
view.expect_payer('مشتری آزمایشی')
|
||||
view.select_destination(account.pk, account.bank.name, identifier)
|
||||
page.get_by_label('واریز مستقل', exact=True).check()
|
||||
view.select_destination(other.pk)
|
||||
view.select_destination(account.pk, account.bank.name, identifier)
|
||||
|
||||
|
||||
def test_staff_can_cancel_then_confirm_rejection_sms(live_server, browser_page, payment_case):
|
||||
customer, account, reviewers = payment_case
|
||||
with browser_page() as page:
|
||||
view = ReceiptCustomerDetailsPage(page, live_server.url)
|
||||
auth = AuthenticationPage(page, live_server.url)
|
||||
auth.login_customer(customer, PASSWORD)
|
||||
view.goto(reverse('payments:receipt_new'))
|
||||
page.get_by_label('واریز مستقل', exact=True).check()
|
||||
view.fill_receipt(account.pk, PNG)
|
||||
view.submit()
|
||||
expect(page.locator('.flash')).to_contain_text('فیش ثبت شد')
|
||||
receipt_id = page.url.rstrip('/').rsplit('/', 1)[-1]
|
||||
page.get_by_role('button', name='خروج', exact=True).first.click()
|
||||
auth.login_staff(reviewers[0], PASSWORD)
|
||||
view.goto(reverse('staff_payments:receipt_detail', args=[receipt_id]))
|
||||
view.reject('تصویر فیش ناخوانا است.', confirm=False)
|
||||
view.reject('تصویر فیش ناخوانا است.', confirm=True)
|
||||
expect(page.get_by_text('تصویر فیش ناخوانا است.', exact=True)).to_be_visible()
|
||||
@@ -78,7 +78,7 @@ def test_revised_receipt_uses_upload_progress(live_server, browser_page, payment
|
||||
today = jdatetime.date.fromgregorian(date=timezone.localdate())
|
||||
client.force_login(customer)
|
||||
response = client.post(reverse('payments:receipt_new'), {
|
||||
'receipt_mode': 'independent', 'paid_amount': '12345', 'destination_account': account.pk,
|
||||
'depositor_name': 'مشتری آزمایشی', 'receipt_mode': 'independent', 'paid_amount': '12345', 'destination_account': account.pk,
|
||||
'paid_day': str(today.day), 'paid_month': str(today.month), 'paid_year': str(today.year),
|
||||
'submission_key': str(uuid.uuid4()),
|
||||
'receipt_image': SimpleUploadedFile('receipt.png', PNG, content_type='image/png'),
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_staff_default_action_tab_and_pagination(live_server, browser_page, paym
|
||||
client.force_login(customer)
|
||||
today = jdatetime.date.fromgregorian(date=timezone.localdate())
|
||||
for _ in range(26):
|
||||
response = client.post(reverse('payments:receipt_new'), {'receipt_mode': 'independent',
|
||||
response = client.post(reverse('payments:receipt_new'), {'depositor_name': 'مشتری آزمایشی', 'receipt_mode': 'independent',
|
||||
'paid_amount': '12345', 'destination_account': account.pk, 'submission_key': str(uuid.uuid4()),
|
||||
'paid_day': str(today.day), 'paid_month': str(today.month), 'paid_year': str(today.year),
|
||||
'receipt_image': SimpleUploadedFile('receipt.png', PNG, content_type='image/png')})
|
||||
|
||||
@@ -77,7 +77,7 @@ def test_all_customer_entry_points_enqueue_once_without_network(case, enabled, c
|
||||
alerts = list(PaymentReceiptAlert.objects.filter(revision__receipt=receipt))
|
||||
assert len(alerts) == 3
|
||||
assert {a.recipient_mobile for a in alerts} == {'', '+989121110001', '+989121110002'}
|
||||
assert alerts[0].context['customer_name'] == 'مشتری آزمایشی'
|
||||
assert alerts[0].context['customer_name'] == 'مشتری آزمایشی (واریزکننده: واریزکننده آزمایشی)'
|
||||
assert alerts[0].context['customer_mobile'] == '09120009999'
|
||||
assert alerts[0].context['received_at'] == jalali_datetime(receipt.revisions.get().created_at)
|
||||
assert alerts[0].context['amount_rial'] == '12,345'
|
||||
@@ -150,7 +150,7 @@ def test_worker_sends_private_photo_with_database_token_and_sms_to_managers(case
|
||||
assert run(alert.job).status == 'SENT'
|
||||
assert len(posted) == 1 and posted[0][0].endswith('/botprivate-payment-token/sendPhoto')
|
||||
assert posted[0][1]['chat_id'] == 'receipt-group' and posted[0][2] == PNG
|
||||
assert 'نام ثبت نشده' in posted[0][1]['caption'] and '09120009999' in posted[0][1]['caption']
|
||||
assert 'واریزکننده آزمایشی' in posted[0][1]['caption'] and '09120009999' in posted[0][1]['caption']
|
||||
assert jalali_datetime(receipt.revisions.get().created_at) in posted[0][1]['caption']
|
||||
assert len(sent) == 2 and {mobile for mobile, _ in sent} == {'+989121110001', '+989121110002'}
|
||||
assert all('12,345' in body and '09120009999' in body for _, body in sent)
|
||||
|
||||
@@ -101,7 +101,7 @@ def case(settings, tmp_path):
|
||||
|
||||
def data(case, linked=True, **extra):
|
||||
today = jdatetime.date.fromgregorian(date=timezone.localdate())
|
||||
return {'receipt_mode':'linked' if linked else 'independent','notice_id':case['notice'].pk if linked else '',
|
||||
return {'depositor_name':'واریزکننده آزمایشی','receipt_mode':'linked' if linked else 'independent','notice_id':case['notice'].pk if linked else '',
|
||||
'paid_amount':'۱۲۳۴۵','destination_account':case['account'].pk,'deposit_id':'000123',
|
||||
'paid_day':str(today.day),'paid_month':str(today.month),'paid_year':str(today.year),
|
||||
'submission_key':str(uuid.uuid4()), **extra}
|
||||
@@ -200,7 +200,7 @@ def test_policy_members_can_reject_without_extra_permissions(case, client, key):
|
||||
detail = client.get(reverse('staff_payments:receipt_detail', args=[receipt.public_id]))
|
||||
assert 'رد فیش' in detail.content.decode()
|
||||
response = client.post(reverse('staff_payments:decision', args=[receipt.public_id]),
|
||||
{'version': 1, 'action': 'REJECT', 'reason': 'تصویر ناخوانا است'})
|
||||
{'version': 1, 'action': 'REJECT', 'reason': 'تصویر ناخوانا است', 'confirm_rejection': '1'})
|
||||
assert response.status_code == 302
|
||||
receipt.refresh_from_db()
|
||||
assert receipt.status == 'REJECTED'
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Receipt identity, rejection outbox and destination warning regressions."""
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.customers.models import CustomerProfile
|
||||
from apps.integrations.sms.base import SMSResult
|
||||
from apps.notifications.management.commands import process_notifications as worker
|
||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||
from apps.payments import services
|
||||
from apps.payments.deposit_identifier import generate_id_algorithm
|
||||
from apps.payments.forms import ReceiptForm
|
||||
from apps.payments.models import PaymentNotification, PaymentReceiptAlert, ReceiptDecision
|
||||
from tests.test_payment_links import open_notice
|
||||
from tests.test_payment_receipt_alerts import enabled, run
|
||||
from tests.test_payments import case, data, submit, upload
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize('via_link', [False, True])
|
||||
def test_payer_prefill_uses_profile_and_stays_editable(case, client, via_link):
|
||||
customer = case['customer']
|
||||
customer.first_name = 'نام حساب'
|
||||
customer.save()
|
||||
CustomerProfile.objects.update_or_create(user=customer, defaults={'first_name': 'علی', 'last_name': 'کریمی'})
|
||||
if via_link:
|
||||
open_notice(client, case)
|
||||
else:
|
||||
client.force_login(customer)
|
||||
page = client.get(reverse('payments:receipt_new'), {'notice': case['notice'].public_id})
|
||||
assert page.status_code == 200
|
||||
assert page.context['form']['depositor_name'].value() == 'علی کریمی'
|
||||
assert not page.context['form'].fields['depositor_name'].disabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize('payer', ['', ' ', '\u200c\u200b', 'الف' * 151])
|
||||
def test_empty_or_invalid_payer_rejected_in_form_and_service(case, payer):
|
||||
form = ReceiptForm(data(case, depositor_name=payer), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert not form.is_valid() and 'depositor_name' in form.errors
|
||||
valid = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert valid.is_valid(), valid.errors
|
||||
with pytest.raises(ValidationError):
|
||||
services.submit_receipt(case['customer'], case['customer'], {**valid.cleaned_data, 'depositor_name': payer})
|
||||
|
||||
|
||||
def test_name_missing_from_post_is_not_silently_prefilled(case, client):
|
||||
customer = case['customer']
|
||||
customer.first_name = 'نام موجود'
|
||||
customer.save()
|
||||
client.force_login(customer)
|
||||
values = data(case)
|
||||
values.pop('depositor_name')
|
||||
response = client.post(reverse('payments:receipt_new'), {**values, 'receipt_image': upload()}, HTTP_ACCEPT='application/json')
|
||||
assert response.status_code == 422
|
||||
assert response.json()['errors']['depositor_name']
|
||||
|
||||
|
||||
def test_receipt_payer_shown_to_staff_and_nonce_cannot_change_identity(case, client):
|
||||
customer = case['customer']
|
||||
customer.first_name = 'نام مشتری'
|
||||
customer.save()
|
||||
values = data(case, depositor_name='شخص واریزکننده')
|
||||
form = ReceiptForm(values, {'receipt_image': upload()}, customer=customer)
|
||||
assert form.is_valid(), form.errors
|
||||
receipt = services.submit_receipt(customer, customer, form.cleaned_data)
|
||||
assert receipt.revisions.get().depositor_name == 'شخص واریزکننده'
|
||||
with pytest.raises(ValidationError):
|
||||
services.submit_receipt(customer, customer, {**form.cleaned_data, 'depositor_name': 'شخص دیگر'})
|
||||
client.force_login(case['first'])
|
||||
for endpoint in [reverse('staff_payments:desk'), reverse('staff_payments:receipt_detail', args=[receipt.public_id])]:
|
||||
content = client.get(endpoint).content.decode()
|
||||
assert 'نام مشتری' in content and 'شخص واریزکننده' in content
|
||||
|
||||
|
||||
def test_revision_keeps_payer_history_and_prefill(case, client):
|
||||
receipt = submit(case)
|
||||
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'اصلاح کنید')
|
||||
client.force_login(case['customer'])
|
||||
endpoint = reverse('payments:receipt_revise', args=[receipt.public_id])
|
||||
assert client.get(endpoint).context['form']['depositor_name'].value() == 'واریزکننده آزمایشی'
|
||||
response = client.post(endpoint, {**data(case, depositor_name='واریزکننده اصلاحشده'), 'version': 1, 'receipt_image': upload()})
|
||||
assert response.status_code == 302
|
||||
assert receipt.revisions.get(version=1).depositor_name == 'واریزکننده آزمایشی'
|
||||
assert receipt.revisions.get(version=2).depositor_name == 'واریزکننده اصلاحشده'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('payer,expected', [('علی کریمی', 'علی کریمی'), ('علي كريمي', 'علی کریمی'), ('رضا احمدی', 'علی کریمی (واریزکننده: رضا احمدی)')])
|
||||
def test_sms_and_bale_include_payer_without_duplicate_name(case, enabled, monkeypatch, payer, expected):
|
||||
customer = case['customer']
|
||||
customer.first_name, customer.last_name = 'علی', 'کریمی'
|
||||
customer.save()
|
||||
form = ReceiptForm(data(case, depositor_name=payer), {'receipt_image': upload()}, customer=customer)
|
||||
assert form.is_valid(), form.errors
|
||||
services.submit_receipt(customer, customer, form.cleaned_data)
|
||||
sent = []
|
||||
provider = SimpleNamespace(send_message=lambda mobile, body: sent.append(body) or SMSResult(True, 'sms-id'))
|
||||
monkeypatch.setattr(worker, 'get_sms_provider', lambda: provider)
|
||||
monkeypatch.setattr('apps.integrations.bale.providers.BaleBotProvider.send_photo',
|
||||
lambda self, chat, image, caption, **kwargs: sent.append(caption) or SMSResult(True, 'bale-id'))
|
||||
for alert in PaymentReceiptAlert.objects.select_related('job'):
|
||||
assert alert.context['customer_name'] == expected
|
||||
assert run(alert.job).status == 'SENT'
|
||||
assert len(sent) == 3
|
||||
assert all(expected in body and body.count('علی کریمی') == 1 for body in sent)
|
||||
|
||||
|
||||
def test_rejection_requires_confirmation_and_queues_exact_reason_once(case, client, monkeypatch):
|
||||
receipt = submit(case)
|
||||
client.force_login(case['first'])
|
||||
endpoint = reverse('staff_payments:decision', args=[receipt.public_id])
|
||||
reason = 'تصویر ناخوانا است؛ لطفاً تصویر واضح ارسال کنید.'
|
||||
network = Mock(side_effect=AssertionError('Must queue, not send during decision'))
|
||||
monkeypatch.setattr(worker, 'get_sms_provider', network)
|
||||
payload = {'version': 1, 'action': 'REJECT', 'reason': reason}
|
||||
client.post(endpoint, payload)
|
||||
receipt.refresh_from_db()
|
||||
assert receipt.status == 'PENDING' and not ReceiptDecision.objects.exists()
|
||||
for _ in range(2):
|
||||
assert client.post(endpoint, {**payload, 'confirm_rejection': '1'}).status_code == 302
|
||||
receipt.refresh_from_db()
|
||||
assert receipt.status == 'REJECTED'
|
||||
job = NotificationJob.objects.get(event_type='PAYMENT_RECEIPT_REJECTED')
|
||||
notification = PaymentNotification.objects.get(job=job)
|
||||
assert notification.context['rejection_reason'] == reason
|
||||
assert notification.cycle.revision.receipt == receipt
|
||||
network.assert_not_called()
|
||||
sent = []
|
||||
monkeypatch.setattr(worker, 'get_sms_provider', lambda: SimpleNamespace(
|
||||
send_message=lambda mobile, body: sent.append((mobile, body)) or SMSResult(True, 'rejection-sms')))
|
||||
assert run(job).status == 'SENT'
|
||||
assert sent[0][0] == case['customer'].mobile and reason in sent[0][1]
|
||||
assert '/p/' in sent[0][1]
|
||||
notification.refresh_from_db()
|
||||
assert notification.attempt_log.count() == 1
|
||||
|
||||
|
||||
def test_rejection_reason_survives_custom_template_and_queue_rollback(case, monkeypatch):
|
||||
receipt = submit(case)
|
||||
with pytest.raises(RuntimeError), transaction.atomic():
|
||||
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'ناخوانا')
|
||||
raise RuntimeError('rollback')
|
||||
assert not NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_REJECTED').exists()
|
||||
assert not ReceiptDecision.objects.exists()
|
||||
template = SMSTemplate.objects.get(event_type='PAYMENT_RECEIPT_REJECTED', unit__isnull=True)
|
||||
template.body = 'فیش {receipt_number} رد شد.'
|
||||
template.save()
|
||||
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'ناخوانا')
|
||||
job = NotificationJob.objects.get(event_type='PAYMENT_RECEIPT_REJECTED')
|
||||
messages = []
|
||||
monkeypatch.setattr(worker, 'get_sms_provider', lambda: SimpleNamespace(
|
||||
send_message=lambda mobile, body: messages.append(body) or SMSResult(True, 'id')))
|
||||
assert run(job).status == 'SENT' and 'ناخوانا' in messages[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('linked', [False, True])
|
||||
def test_warning_uses_selected_bank_and_generated_customer_identifier(case, client, linked):
|
||||
client.force_login(case['customer'])
|
||||
params = {'notice': case['notice'].public_id} if linked else {}
|
||||
page = client.get(reverse('payments:receipt_new'), params)
|
||||
assert page.context['customer_deposit_id'] == generate_id_algorithm(case['customer'].local_mobile)
|
||||
assert page.context['account_requirements'][str(case['account'].pk)] == {'required': True, 'bank': case['account'].bank.name}
|
||||
assert 'ممکن است مبلغ واریزی به حساب شما برگشت بخورد' in page.content.decode()
|
||||
Reference in new issue
Block a user