dana-python 2.2.0__py3-none-any.whl → 2.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dana/disbursement/v1/api/disbursement_api.py +49 -61
- dana/disbursement/v1/custom_validation.py +94 -112
- dana/payment_gateway/v1/custom_validation.py +98 -48
- dana/widget/v1/util.py +203 -154
- {dana_python-2.2.0.dist-info → dana_python-2.2.2.dist-info}/METADATA +1 -1
- {dana_python-2.2.0.dist-info → dana_python-2.2.2.dist-info}/RECORD +9 -9
- {dana_python-2.2.0.dist-info → dana_python-2.2.2.dist-info}/WHEEL +1 -1
- {dana_python-2.2.0.dist-info → dana_python-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {dana_python-2.2.0.dist-info → dana_python-2.2.2.dist-info}/top_level.txt +0 -0
|
@@ -54,7 +54,7 @@ from dana.utils.snap_header import SnapHeader
|
|
|
54
54
|
from dana.disbursement.v1.custom_validation import (
|
|
55
55
|
custom_validation,
|
|
56
56
|
custom_validation_response,
|
|
57
|
-
|
|
57
|
+
enrich_disbursement_error,
|
|
58
58
|
)
|
|
59
59
|
from dana.exceptions import ApiException
|
|
60
60
|
|
|
@@ -144,11 +144,10 @@ class DisbursementApi:
|
|
|
144
144
|
pass
|
|
145
145
|
return api_response.data
|
|
146
146
|
except ApiException as e:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
raise e
|
|
147
|
+
try:
|
|
148
|
+
raise enrich_disbursement_error(bank_account_inquiry_request, e)
|
|
149
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
150
|
+
raise e
|
|
152
151
|
raise e
|
|
153
152
|
|
|
154
153
|
|
|
@@ -224,11 +223,10 @@ class DisbursementApi:
|
|
|
224
223
|
pass
|
|
225
224
|
return api_response
|
|
226
225
|
except ApiException as e:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
raise e
|
|
226
|
+
try:
|
|
227
|
+
raise enrich_disbursement_error(bank_account_inquiry_request, e)
|
|
228
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
229
|
+
raise e
|
|
232
230
|
raise e
|
|
233
231
|
|
|
234
232
|
|
|
@@ -464,11 +462,10 @@ class DisbursementApi:
|
|
|
464
462
|
pass
|
|
465
463
|
return api_response.data
|
|
466
464
|
except ApiException as e:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
raise e
|
|
465
|
+
try:
|
|
466
|
+
raise enrich_disbursement_error(dana_account_inquiry_request, e)
|
|
467
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
468
|
+
raise e
|
|
472
469
|
raise e
|
|
473
470
|
|
|
474
471
|
|
|
@@ -544,11 +541,10 @@ class DisbursementApi:
|
|
|
544
541
|
pass
|
|
545
542
|
return api_response
|
|
546
543
|
except ApiException as e:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
raise e
|
|
544
|
+
try:
|
|
545
|
+
raise enrich_disbursement_error(dana_account_inquiry_request, e)
|
|
546
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
547
|
+
raise e
|
|
552
548
|
raise e
|
|
553
549
|
|
|
554
550
|
|
|
@@ -785,11 +781,10 @@ class DisbursementApi:
|
|
|
785
781
|
pass
|
|
786
782
|
return api_response.data
|
|
787
783
|
except ApiException as e:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
raise e
|
|
784
|
+
try:
|
|
785
|
+
raise enrich_disbursement_error(transfer_to_bank_request, e)
|
|
786
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
787
|
+
raise e
|
|
793
788
|
raise e
|
|
794
789
|
|
|
795
790
|
|
|
@@ -866,11 +861,10 @@ class DisbursementApi:
|
|
|
866
861
|
pass
|
|
867
862
|
return api_response
|
|
868
863
|
except ApiException as e:
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
raise e
|
|
864
|
+
try:
|
|
865
|
+
raise enrich_disbursement_error(transfer_to_bank_request, e)
|
|
866
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
867
|
+
raise e
|
|
874
868
|
raise e
|
|
875
869
|
|
|
876
870
|
|
|
@@ -1107,11 +1101,10 @@ class DisbursementApi:
|
|
|
1107
1101
|
pass
|
|
1108
1102
|
return api_response.data
|
|
1109
1103
|
except ApiException as e:
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
raise e
|
|
1104
|
+
try:
|
|
1105
|
+
raise enrich_disbursement_error(transfer_to_bank_inquiry_status_request, e)
|
|
1106
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1107
|
+
raise e
|
|
1115
1108
|
raise e
|
|
1116
1109
|
|
|
1117
1110
|
|
|
@@ -1187,11 +1180,10 @@ class DisbursementApi:
|
|
|
1187
1180
|
pass
|
|
1188
1181
|
return api_response
|
|
1189
1182
|
except ApiException as e:
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
raise e
|
|
1183
|
+
try:
|
|
1184
|
+
raise enrich_disbursement_error(transfer_to_bank_inquiry_status_request, e)
|
|
1185
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1186
|
+
raise e
|
|
1195
1187
|
raise e
|
|
1196
1188
|
|
|
1197
1189
|
|
|
@@ -1427,11 +1419,10 @@ class DisbursementApi:
|
|
|
1427
1419
|
pass
|
|
1428
1420
|
return api_response.data
|
|
1429
1421
|
except ApiException as e:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
raise e
|
|
1422
|
+
try:
|
|
1423
|
+
raise enrich_disbursement_error(transfer_to_dana_request, e)
|
|
1424
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1425
|
+
raise e
|
|
1435
1426
|
raise e
|
|
1436
1427
|
|
|
1437
1428
|
|
|
@@ -1507,11 +1498,10 @@ class DisbursementApi:
|
|
|
1507
1498
|
pass
|
|
1508
1499
|
return api_response
|
|
1509
1500
|
except ApiException as e:
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
raise e
|
|
1501
|
+
try:
|
|
1502
|
+
raise enrich_disbursement_error(transfer_to_dana_request, e)
|
|
1503
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1504
|
+
raise e
|
|
1515
1505
|
raise e
|
|
1516
1506
|
|
|
1517
1507
|
|
|
@@ -1747,11 +1737,10 @@ class DisbursementApi:
|
|
|
1747
1737
|
pass
|
|
1748
1738
|
return api_response.data
|
|
1749
1739
|
except ApiException as e:
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
raise e
|
|
1740
|
+
try:
|
|
1741
|
+
raise enrich_disbursement_error(transfer_to_dana_inquiry_status_request, e)
|
|
1742
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1743
|
+
raise e
|
|
1755
1744
|
raise e
|
|
1756
1745
|
|
|
1757
1746
|
|
|
@@ -1827,11 +1816,10 @@ class DisbursementApi:
|
|
|
1827
1816
|
pass
|
|
1828
1817
|
return api_response
|
|
1829
1818
|
except ApiException as e:
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
raise e
|
|
1819
|
+
try:
|
|
1820
|
+
raise enrich_disbursement_error(transfer_to_dana_inquiry_status_request, e)
|
|
1821
|
+
except (ImportError, ModuleNotFoundError, NameError):
|
|
1822
|
+
raise e
|
|
1835
1823
|
raise e
|
|
1836
1824
|
|
|
1837
1825
|
|
|
@@ -27,16 +27,24 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
27
27
|
from dana.exceptions import ApiException
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
SANDBOX_BENEFICIARY_ACCOUNT_NUMBER = '2460888509'
|
|
31
|
-
SANDBOX_BENEFICIARY_BANK_CODE = '014'
|
|
32
|
-
# Sandbox maximum amount (major units) for Disbursement.
|
|
33
|
-
SANDBOX_MAX_AMOUNT = 20000000
|
|
34
30
|
ALLOWED_ACCOUNT_TYPES = frozenset({
|
|
35
31
|
'MERCHANT_DEPOSIT_ACCOUNT',
|
|
36
32
|
'SETTLEMENT_ACCOUNT',
|
|
37
33
|
'DIVISION_DEPOSIT_ACCOUNT',
|
|
38
34
|
})
|
|
39
35
|
|
|
36
|
+
SANDBOX_POSITIVE_BENEFICIARY_ACCOUNT_NUMBER = '2460888509'
|
|
37
|
+
SANDBOX_POSITIVE_BENEFICIARY_BANK_CODE = '014'
|
|
38
|
+
SANDBOX_MAX_AMOUNT = 20000000
|
|
39
|
+
|
|
40
|
+
SANDBOX_POSITIVE_BANK_HINT = (
|
|
41
|
+
f'For testing positive case in sandbox use beneficiaryAccountNumber '
|
|
42
|
+
f'{SANDBOX_POSITIVE_BENEFICIARY_ACCOUNT_NUMBER} and beneficiaryBankCode '
|
|
43
|
+
f'{SANDBOX_POSITIVE_BENEFICIARY_BANK_CODE}'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
SANDBOX_AMOUNT_MAX_HINT = f'In sandbox, amount.value must not exceed {SANDBOX_MAX_AMOUNT}'
|
|
47
|
+
|
|
40
48
|
SANDBOX_DANA_BALANCE_LIMIT_HINT = (
|
|
41
49
|
'Make sure DANA balance not exceeding limit of 21000000 after topup'
|
|
42
50
|
)
|
|
@@ -62,7 +70,7 @@ def _append_sandbox_hint(
|
|
|
62
70
|
hint: str,
|
|
63
71
|
*already_present_markers: str,
|
|
64
72
|
) -> str:
|
|
65
|
-
"""Always append
|
|
73
|
+
"""Always append. Uses markers to avoid duplicates. No 150-char truncate."""
|
|
66
74
|
msg = (response_message or '').strip()
|
|
67
75
|
lower_msg = msg.lower()
|
|
68
76
|
for marker in already_present_markers:
|
|
@@ -75,30 +83,11 @@ def _append_sandbox_hint(
|
|
|
75
83
|
return f'{msg}. {hint}'
|
|
76
84
|
|
|
77
85
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def validate_sandbox_amount(request: Any) -> None:
|
|
84
|
-
"""In sandbox, amount.value must not exceed SANDBOX_MAX_AMOUNT."""
|
|
85
|
-
if request is None or not _is_sandbox():
|
|
86
|
-
return
|
|
87
|
-
amount = getattr(request, 'amount', None)
|
|
88
|
-
if amount is None:
|
|
89
|
-
return
|
|
90
|
-
value = getattr(amount, 'value', None)
|
|
91
|
-
if value is None or str(value).strip() == '':
|
|
92
|
-
return
|
|
93
|
-
try:
|
|
94
|
-
parsed = float(value)
|
|
95
|
-
except (TypeError, ValueError):
|
|
96
|
-
return
|
|
97
|
-
if parsed > SANDBOX_MAX_AMOUNT:
|
|
98
|
-
raise ApiException(status=0, contexts=[_ctx(
|
|
99
|
-
'amount.value',
|
|
100
|
-
f'in sandbox, amount.value must not exceed {SANDBOX_MAX_AMOUNT}; got {value!r}',
|
|
101
|
-
)])
|
|
86
|
+
def _should_append_dana_balance_hint(response_code: Any, response_message: Any) -> bool:
|
|
87
|
+
msg = str(response_message or '').lower()
|
|
88
|
+
if 'exceed' in msg or 'melebihi' in msg:
|
|
89
|
+
return True
|
|
90
|
+
return str(response_code or '').strip() == '4033802'
|
|
102
91
|
|
|
103
92
|
|
|
104
93
|
def _require_non_empty(value: Any, field_path: str) -> None:
|
|
@@ -213,63 +202,6 @@ def validate_account_type_transfer_to_dana(request: Any) -> None:
|
|
|
213
202
|
_validate_account_type_value(current, 'additionalInfo.accountType')
|
|
214
203
|
|
|
215
204
|
|
|
216
|
-
def validate_sandbox_beneficiary_bank_account_inquiry(request: Any) -> None:
|
|
217
|
-
"""In sandbox, beneficiaryAccountNumber must be 2460888509 and beneficiaryBankCode must be 014."""
|
|
218
|
-
if request is None or not _is_sandbox():
|
|
219
|
-
return
|
|
220
|
-
contexts: List[Dict[str, str]] = []
|
|
221
|
-
account_number = _trim_str(getattr(request, 'beneficiary_account_number', None))
|
|
222
|
-
if account_number != SANDBOX_BENEFICIARY_ACCOUNT_NUMBER:
|
|
223
|
-
contexts.append(_ctx(
|
|
224
|
-
'beneficiaryAccountNumber',
|
|
225
|
-
(
|
|
226
|
-
f'in sandbox, beneficiaryAccountNumber must be {SANDBOX_BENEFICIARY_ACCOUNT_NUMBER}; '
|
|
227
|
-
f'got {getattr(request, "beneficiary_account_number", None)!r}'
|
|
228
|
-
),
|
|
229
|
-
))
|
|
230
|
-
additional_info = getattr(request, 'additional_info', None)
|
|
231
|
-
bank_code = _trim_str(
|
|
232
|
-
getattr(additional_info, 'beneficiary_bank_code', None) if additional_info is not None else None
|
|
233
|
-
)
|
|
234
|
-
if bank_code != SANDBOX_BENEFICIARY_BANK_CODE:
|
|
235
|
-
contexts.append(_ctx(
|
|
236
|
-
'additionalInfo.beneficiaryBankCode',
|
|
237
|
-
(
|
|
238
|
-
f'in sandbox, additionalInfo.beneficiaryBankCode must be {SANDBOX_BENEFICIARY_BANK_CODE}; '
|
|
239
|
-
f'got {bank_code!r}'
|
|
240
|
-
),
|
|
241
|
-
))
|
|
242
|
-
if contexts:
|
|
243
|
-
raise ApiException(status=0, contexts=contexts)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def validate_sandbox_beneficiary_transfer_to_bank(request: Any) -> None:
|
|
247
|
-
"""In sandbox, beneficiaryAccountNumber must be 2460888509 and beneficiaryBankCode must be 014."""
|
|
248
|
-
if request is None or not _is_sandbox():
|
|
249
|
-
return
|
|
250
|
-
contexts: List[Dict[str, str]] = []
|
|
251
|
-
account_number = _trim_str(getattr(request, 'beneficiary_account_number', None))
|
|
252
|
-
if account_number != SANDBOX_BENEFICIARY_ACCOUNT_NUMBER:
|
|
253
|
-
contexts.append(_ctx(
|
|
254
|
-
'beneficiaryAccountNumber',
|
|
255
|
-
(
|
|
256
|
-
f'in sandbox, beneficiaryAccountNumber must be {SANDBOX_BENEFICIARY_ACCOUNT_NUMBER}; '
|
|
257
|
-
f'got {getattr(request, "beneficiary_account_number", None)!r}'
|
|
258
|
-
),
|
|
259
|
-
))
|
|
260
|
-
bank_code = _trim_str(getattr(request, 'beneficiary_bank_code', None))
|
|
261
|
-
if bank_code != SANDBOX_BENEFICIARY_BANK_CODE:
|
|
262
|
-
contexts.append(_ctx(
|
|
263
|
-
'beneficiaryBankCode',
|
|
264
|
-
(
|
|
265
|
-
f'in sandbox, beneficiaryBankCode must be {SANDBOX_BENEFICIARY_BANK_CODE}; '
|
|
266
|
-
f'got {getattr(request, "beneficiary_bank_code", None)!r}'
|
|
267
|
-
),
|
|
268
|
-
))
|
|
269
|
-
if contexts:
|
|
270
|
-
raise ApiException(status=0, contexts=contexts)
|
|
271
|
-
|
|
272
|
-
|
|
273
205
|
def validate_required_additional_info_not_empty_bank_account_inquiry(request: Any) -> None:
|
|
274
206
|
if request is None:
|
|
275
207
|
return
|
|
@@ -323,26 +255,20 @@ validation_registry: Dict[str, List[Callable[[Any], None]]] = {
|
|
|
323
255
|
'BankAccountInquiryRequest': [
|
|
324
256
|
strip_sandbox_ignored_fields_bank_account_inquiry,
|
|
325
257
|
validate_account_type_bank_account_inquiry,
|
|
326
|
-
validate_sandbox_beneficiary_bank_account_inquiry,
|
|
327
|
-
validate_sandbox_amount,
|
|
328
258
|
validate_required_additional_info_not_empty_bank_account_inquiry,
|
|
329
259
|
],
|
|
330
260
|
'TransferToBankRequest': [
|
|
331
261
|
strip_sandbox_ignored_fields_transfer_to_bank,
|
|
332
262
|
validate_account_type_transfer_to_bank,
|
|
333
|
-
validate_sandbox_beneficiary_transfer_to_bank,
|
|
334
|
-
validate_sandbox_amount,
|
|
335
263
|
validate_required_additional_info_not_empty_transfer_to_bank,
|
|
336
264
|
],
|
|
337
265
|
'TransferToDanaRequest': [
|
|
338
266
|
strip_sandbox_ignored_fields_transfer_to_dana,
|
|
339
267
|
validate_account_type_transfer_to_dana,
|
|
340
|
-
validate_sandbox_amount,
|
|
341
268
|
validate_required_additional_info_not_empty_transfer_to_dana,
|
|
342
269
|
],
|
|
343
270
|
'DanaAccountInquiryRequest': [
|
|
344
271
|
strip_sandbox_ignored_fields_dana_account_inquiry,
|
|
345
|
-
validate_sandbox_amount,
|
|
346
272
|
validate_required_additional_info_not_empty_dana_account_inquiry,
|
|
347
273
|
],
|
|
348
274
|
}
|
|
@@ -379,31 +305,62 @@ def custom_validation(request: Any) -> None:
|
|
|
379
305
|
)
|
|
380
306
|
|
|
381
307
|
|
|
382
|
-
def
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
308
|
+
def _is_bank_transfer_request(request: Any) -> bool:
|
|
309
|
+
return request.__class__.__name__ in ('BankAccountInquiryRequest', 'TransferToBankRequest')
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _should_append_positive_bank_hint(response_code: Any) -> bool:
|
|
313
|
+
return str(response_code or '').strip().startswith('500')
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _should_append_amount_max_hint(response_message: Any) -> bool:
|
|
317
|
+
msg = str(response_message or '').lower()
|
|
318
|
+
return 'exceed' in msg or 'melebihi' in msg
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _apply_sandbox_disbursement_hints(request: Any, response: Any) -> None:
|
|
388
322
|
if not hasattr(response, 'response_message'):
|
|
389
323
|
return
|
|
390
324
|
response_code = getattr(response, 'response_code', None)
|
|
391
|
-
|
|
325
|
+
response_message = getattr(response, 'response_message', None)
|
|
326
|
+
|
|
327
|
+
updated = str(response_message or '')
|
|
328
|
+
|
|
329
|
+
if _is_bank_transfer_request(request) and _should_append_positive_bank_hint(response_code):
|
|
330
|
+
updated = _append_sandbox_hint(
|
|
331
|
+
updated,
|
|
332
|
+
SANDBOX_POSITIVE_BANK_HINT,
|
|
333
|
+
SANDBOX_POSITIVE_BENEFICIARY_ACCOUNT_NUMBER,
|
|
334
|
+
f'beneficiarybankcode {SANDBOX_POSITIVE_BENEFICIARY_BANK_CODE}',
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if request.__class__.__name__ == 'TransferToDanaRequest':
|
|
338
|
+
if _should_append_dana_balance_hint(response_code, response_message):
|
|
339
|
+
updated = _append_sandbox_hint(
|
|
340
|
+
updated,
|
|
341
|
+
SANDBOX_DANA_BALANCE_LIMIT_HINT,
|
|
342
|
+
'21000000',
|
|
343
|
+
'after topup',
|
|
344
|
+
)
|
|
345
|
+
elif _should_append_amount_max_hint(response_message):
|
|
346
|
+
updated = _append_sandbox_hint(updated, SANDBOX_AMOUNT_MAX_HINT, str(SANDBOX_MAX_AMOUNT))
|
|
347
|
+
|
|
348
|
+
if updated != str(response_message or ''):
|
|
349
|
+
response.response_message = updated
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def custom_validation_response(request: Any, response: Any) -> None:
|
|
353
|
+
"""Augment Disbursement responses in sandbox with account/amount guidance."""
|
|
354
|
+
if not _is_sandbox() or request is None or response is None:
|
|
392
355
|
return
|
|
393
|
-
response
|
|
394
|
-
response.response_message,
|
|
395
|
-
SANDBOX_DANA_BALANCE_LIMIT_HINT,
|
|
396
|
-
'21000000',
|
|
397
|
-
'after topup',
|
|
398
|
-
)
|
|
356
|
+
_apply_sandbox_disbursement_hints(request, response)
|
|
399
357
|
|
|
400
358
|
|
|
401
|
-
def
|
|
402
|
-
"""Enrich
|
|
359
|
+
def enrich_disbursement_error(request: Any, exc: ApiException) -> ApiException:
|
|
360
|
+
"""Enrich Disbursement HTTP errors in sandbox (keeps exception; updates reason / data).
|
|
361
|
+
"""
|
|
403
362
|
if not _is_sandbox() or request is None or exc is None:
|
|
404
363
|
return exc
|
|
405
|
-
if request.__class__.__name__ != 'TransferToDanaRequest':
|
|
406
|
-
return exc
|
|
407
364
|
|
|
408
365
|
body = exc.body
|
|
409
366
|
if body is None or body == '':
|
|
@@ -420,13 +377,24 @@ def enrich_transfer_to_dana_error(request: Any, exc: ApiException) -> ApiExcepti
|
|
|
420
377
|
if not isinstance(payload, dict):
|
|
421
378
|
return exc
|
|
422
379
|
|
|
380
|
+
partner_reference_no = str(
|
|
381
|
+
payload.get('partnerReferenceNo')
|
|
382
|
+
or payload.get('originalPartnerReferenceNo')
|
|
383
|
+
or ''
|
|
384
|
+
)
|
|
423
385
|
response = SimpleNamespace(
|
|
424
386
|
response_code=str(payload.get('responseCode') or ''),
|
|
425
387
|
response_message=str(payload.get('responseMessage') or ''),
|
|
426
|
-
partner_reference_no=
|
|
388
|
+
partner_reference_no=partner_reference_no,
|
|
427
389
|
)
|
|
390
|
+
original_msg = response.response_message
|
|
428
391
|
custom_validation_response(request, response)
|
|
429
392
|
|
|
393
|
+
reason = exc.reason
|
|
394
|
+
if response.response_message and response.response_message != original_msg:
|
|
395
|
+
status = exc.status if exc.status is not None else ''
|
|
396
|
+
reason = f'{status}: {response.response_message}'
|
|
397
|
+
|
|
430
398
|
enriched_data = {
|
|
431
399
|
'responseCode': response.response_code,
|
|
432
400
|
'responseMessage': response.response_message,
|
|
@@ -435,12 +403,26 @@ def enrich_transfer_to_dana_error(request: Any, exc: ApiException) -> ApiExcepti
|
|
|
435
403
|
if isinstance(exc.data, dict):
|
|
436
404
|
enriched_data = {**exc.data, **enriched_data}
|
|
437
405
|
|
|
406
|
+
enriched_body = body_str
|
|
407
|
+
if response.response_message != original_msg:
|
|
408
|
+
enriched_payload = dict(payload)
|
|
409
|
+
enriched_payload['responseCode'] = enriched_data['responseCode']
|
|
410
|
+
enriched_payload['responseMessage'] = enriched_data['responseMessage']
|
|
411
|
+
if enriched_data.get('partnerReferenceNo'):
|
|
412
|
+
enriched_payload['partnerReferenceNo'] = enriched_data['partnerReferenceNo']
|
|
413
|
+
enriched_body = json.dumps(enriched_payload)
|
|
414
|
+
|
|
438
415
|
# Preserve subclass (e.g. NotFoundException for HTTP 404) so callers can catch by type
|
|
439
416
|
enriched = type(exc)(
|
|
440
417
|
status=exc.status,
|
|
441
|
-
reason=
|
|
442
|
-
body=
|
|
418
|
+
reason=reason,
|
|
419
|
+
body=enriched_body,
|
|
443
420
|
data=enriched_data,
|
|
444
421
|
)
|
|
445
422
|
enriched.headers = getattr(exc, 'headers', None)
|
|
446
423
|
return enriched
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def enrich_transfer_to_dana_error(request: Any, exc: ApiException) -> ApiException:
|
|
427
|
+
"""Enrich TransferToDana HTTP errors in sandbox (alias for enrich_disbursement_error)."""
|
|
428
|
+
return enrich_disbursement_error(request, exc)
|
|
@@ -53,16 +53,18 @@ EWALLET_PAY_OPTIONS: Set[str] = {
|
|
|
53
53
|
|
|
54
54
|
SANDBOX_QRIS_GUIDANCE_HINT_SUCCESS = (
|
|
55
55
|
'If you want to use QRIS and it is not showing in payment methods, make sure you already fill '
|
|
56
|
-
'externalStoreId. See https://dashboard.dana.id/sandbox/submerchants in the external shop id section.'
|
|
56
|
+
'externalStoreId. See https://dashboard.dana.id/sandbox/submerchants in the external shop id section. '
|
|
57
|
+
'For QRIS, partnerReferenceNo max is 25 chars.'
|
|
57
58
|
)
|
|
58
59
|
SANDBOX_QRIS_GUIDANCE_HINT_ERROR = (
|
|
59
60
|
'If you want to use QRIS, make sure you fill externalStoreId. See '
|
|
60
61
|
'https://dashboard.dana.id/sandbox/submerchants in the external shop id section. '
|
|
61
62
|
'For QRIS, partnerReferenceNo max is 25 chars.'
|
|
62
63
|
)
|
|
63
|
-
|
|
64
|
-
'
|
|
65
|
-
'https://dashboard.dana.id/sandbox/submerchants
|
|
64
|
+
SANDBOX_NOT_FOUND_STORE_GUIDANCE_HINT = (
|
|
65
|
+
'If you are using QRIS / making order for your store / submerchant, make sure externalStoreId / '
|
|
66
|
+
'subMerchant exists. See https://dashboard.dana.id/sandbox/submerchants external shop id section '
|
|
67
|
+
'for external store id and external division id section for submerchant'
|
|
66
68
|
)
|
|
67
69
|
|
|
68
70
|
|
|
@@ -128,6 +130,81 @@ def _is_business_error_response(response_code: Any) -> bool:
|
|
|
128
130
|
return code == '' or not code.startswith('200')
|
|
129
131
|
|
|
130
132
|
|
|
133
|
+
def _is_successful_snap_response(response_code: Any) -> bool:
|
|
134
|
+
return str(response_code or '').strip().startswith('200')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _should_append_store_submerchant_hint(response_code: Any, response_message: Any) -> bool:
|
|
138
|
+
msg = str(response_message or '').lower().strip()
|
|
139
|
+
if 'invalid merchant' in msg:
|
|
140
|
+
return True
|
|
141
|
+
return str(response_code or '').strip() == '4045408'
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _should_append_qris_error_hint(response_code: Any, response_message: Any) -> bool:
|
|
145
|
+
code = str(response_code or '').strip()
|
|
146
|
+
if code.startswith('500'):
|
|
147
|
+
return True
|
|
148
|
+
if not code.startswith('400'):
|
|
149
|
+
return False
|
|
150
|
+
msg = str(response_message or '').lower()
|
|
151
|
+
return any(marker in msg for marker in ('reference', 'store', 'merchant'))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _apply_sandbox_create_order_hints(request: Any, response: Any) -> None:
|
|
155
|
+
"""Attach sandbox tips by SNAP response class (mirrors Go applySandboxCreateOrderHints)."""
|
|
156
|
+
if response is None or not hasattr(response, 'response_message'):
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
response_code = getattr(response, 'response_code', None)
|
|
160
|
+
response_message = getattr(response, 'response_message', None)
|
|
161
|
+
|
|
162
|
+
code = str(response_code or '').strip()
|
|
163
|
+
|
|
164
|
+
if _is_successful_snap_response(code):
|
|
165
|
+
if request.__class__.__name__ == 'CreateOrderByRedirectRequest':
|
|
166
|
+
external_store_id = getattr(request, 'external_store_id', None)
|
|
167
|
+
if not (external_store_id and str(external_store_id).strip()):
|
|
168
|
+
object.__setattr__(
|
|
169
|
+
response,
|
|
170
|
+
'response_message',
|
|
171
|
+
_append_sandbox_hint(
|
|
172
|
+
response.response_message,
|
|
173
|
+
SANDBOX_QRIS_GUIDANCE_HINT_SUCCESS,
|
|
174
|
+
'externalstoreid',
|
|
175
|
+
'partnerreferenceno',
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if _should_append_store_submerchant_hint(code, response_message):
|
|
181
|
+
object.__setattr__(
|
|
182
|
+
response,
|
|
183
|
+
'response_message',
|
|
184
|
+
_append_sandbox_hint(
|
|
185
|
+
response.response_message,
|
|
186
|
+
SANDBOX_NOT_FOUND_STORE_GUIDANCE_HINT,
|
|
187
|
+
'externalstoreid',
|
|
188
|
+
'submerchant',
|
|
189
|
+
'external division id',
|
|
190
|
+
'external shop id',
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if _should_append_qris_error_hint(code, response_message):
|
|
196
|
+
object.__setattr__(
|
|
197
|
+
response,
|
|
198
|
+
'response_message',
|
|
199
|
+
_append_sandbox_hint(
|
|
200
|
+
response.response_message,
|
|
201
|
+
SANDBOX_QRIS_GUIDANCE_HINT_ERROR,
|
|
202
|
+
'externalstoreid',
|
|
203
|
+
'partnerreferenceno',
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
131
208
|
def validate_additional_info_required(request: Any) -> None:
|
|
132
209
|
if request is None:
|
|
133
210
|
return
|
|
@@ -523,40 +600,12 @@ def custom_validation(request: Any) -> None:
|
|
|
523
600
|
|
|
524
601
|
|
|
525
602
|
def custom_validation_response(request: Any, response: Any) -> None:
|
|
526
|
-
"""Augment CreateOrder responses in sandbox
|
|
603
|
+
"""Augment CreateOrder responses in sandbox with code-specific tips."""
|
|
527
604
|
if not _is_sandbox() or request is None or response is None:
|
|
528
605
|
return
|
|
529
606
|
if not hasattr(response, 'response_message'):
|
|
530
607
|
return
|
|
531
|
-
|
|
532
|
-
# SUCCESS QRIS hint ONLY for CreateOrderByRedirectRequest without externalStoreId
|
|
533
|
-
if request.__class__.__name__ == 'CreateOrderByRedirectRequest':
|
|
534
|
-
external_store_id = getattr(request, 'external_store_id', None)
|
|
535
|
-
if not (external_store_id and str(external_store_id).strip()):
|
|
536
|
-
object.__setattr__(
|
|
537
|
-
response,
|
|
538
|
-
'response_message',
|
|
539
|
-
_append_sandbox_hint(
|
|
540
|
-
response.response_message,
|
|
541
|
-
SANDBOX_QRIS_GUIDANCE_HINT_SUCCESS,
|
|
542
|
-
'externalstoreid',
|
|
543
|
-
),
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
sub_merchant_id = getattr(request, 'sub_merchant_id', None)
|
|
547
|
-
if sub_merchant_id and str(sub_merchant_id).strip():
|
|
548
|
-
response_code = getattr(response, 'response_code', None)
|
|
549
|
-
if _is_business_error_response(response_code):
|
|
550
|
-
object.__setattr__(
|
|
551
|
-
response,
|
|
552
|
-
'response_message',
|
|
553
|
-
_append_sandbox_hint(
|
|
554
|
-
response.response_message,
|
|
555
|
-
SANDBOX_SUB_MERCHANT_ID_GUIDANCE_HINT,
|
|
556
|
-
'submerchantid',
|
|
557
|
-
'externaldivisionid',
|
|
558
|
-
),
|
|
559
|
-
)
|
|
608
|
+
_apply_sandbox_create_order_hints(request, response)
|
|
560
609
|
|
|
561
610
|
|
|
562
611
|
def enrich_create_order_error(request: Any, exc: ApiException) -> ApiException:
|
|
@@ -584,21 +633,13 @@ def enrich_create_order_error(request: Any, exc: ApiException) -> ApiException:
|
|
|
584
633
|
response_message=str(payload.get('responseMessage') or ''),
|
|
585
634
|
partner_reference_no=str(payload.get('partnerReferenceNo') or ''),
|
|
586
635
|
)
|
|
587
|
-
|
|
636
|
+
original_msg = response.response_message
|
|
637
|
+
_apply_sandbox_create_order_hints(request, response)
|
|
588
638
|
|
|
589
639
|
reason = exc.reason
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if not (external_store_id and str(external_store_id).strip()):
|
|
594
|
-
hinted = _append_sandbox_hint(
|
|
595
|
-
'',
|
|
596
|
-
SANDBOX_QRIS_GUIDANCE_HINT_ERROR,
|
|
597
|
-
'externalstoreid',
|
|
598
|
-
'partnerreferenceno',
|
|
599
|
-
)
|
|
600
|
-
status = exc.status if exc.status is not None else ''
|
|
601
|
-
reason = f'{status}: {hinted}'
|
|
640
|
+
if response.response_message and response.response_message != original_msg:
|
|
641
|
+
status = exc.status if exc.status is not None else ''
|
|
642
|
+
reason = f'{status}: {response.response_message}'
|
|
602
643
|
|
|
603
644
|
enriched_data = {
|
|
604
645
|
'responseCode': response.response_code,
|
|
@@ -608,11 +649,20 @@ def enrich_create_order_error(request: Any, exc: ApiException) -> ApiException:
|
|
|
608
649
|
if isinstance(exc.data, dict):
|
|
609
650
|
enriched_data = {**exc.data, **enriched_data}
|
|
610
651
|
|
|
652
|
+
enriched_body = body_str
|
|
653
|
+
if response.response_message != original_msg:
|
|
654
|
+
enriched_payload = dict(payload)
|
|
655
|
+
enriched_payload['responseCode'] = enriched_data['responseCode']
|
|
656
|
+
enriched_payload['responseMessage'] = enriched_data['responseMessage']
|
|
657
|
+
if enriched_data.get('partnerReferenceNo'):
|
|
658
|
+
enriched_payload['partnerReferenceNo'] = enriched_data['partnerReferenceNo']
|
|
659
|
+
enriched_body = json.dumps(enriched_payload)
|
|
660
|
+
|
|
611
661
|
# Preserve subclass (e.g. NotFoundException for HTTP 404) so callers can catch by type
|
|
612
662
|
enriched = type(exc)(
|
|
613
663
|
status=exc.status,
|
|
614
664
|
reason=reason,
|
|
615
|
-
body=
|
|
665
|
+
body=enriched_body,
|
|
616
666
|
data=enriched_data,
|
|
617
667
|
)
|
|
618
668
|
enriched.headers = getattr(exc, 'headers', None)
|
dana/widget/v1/util.py
CHANGED
|
@@ -31,7 +31,7 @@ import base64
|
|
|
31
31
|
import hashlib
|
|
32
32
|
import urllib.parse
|
|
33
33
|
from datetime import datetime, timezone, timedelta
|
|
34
|
-
from typing import Optional, Dict, Any
|
|
34
|
+
from typing import Optional, Dict, Any
|
|
35
35
|
from dana.widget.v1.models import WidgetPaymentResponse, ApplyOTTResponse
|
|
36
36
|
from dana.utils.snap_header import SnapHeader
|
|
37
37
|
|
|
@@ -52,7 +52,7 @@ except ImportError:
|
|
|
52
52
|
class Mode:
|
|
53
53
|
API = "API"
|
|
54
54
|
DEEPLINK = "DEEPLINK"
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
class TerminalType:
|
|
57
57
|
WEB = "WEB"
|
|
58
58
|
|
|
@@ -61,223 +61,272 @@ class Util:
|
|
|
61
61
|
"""
|
|
62
62
|
Utility class for the Dana Widget API.
|
|
63
63
|
"""
|
|
64
|
-
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
_OAUTH_QUERY_ESCAPE_KEYS = frozenset(
|
|
66
|
+
{"redirectUrl", "timestamp", "seamlessData", "seamlessSign"}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def generate_channel_id() -> str:
|
|
71
|
+
"""Channel ID for widget OAuth URLs (max 5 chars). Matches Go GenerateChannelId."""
|
|
72
|
+
channel_id = os.environ.get("CHANNEL_ID") or "95221"
|
|
73
|
+
return channel_id[:5]
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def generate_scopes() -> str:
|
|
77
|
+
"""OAuth scopes based on environment. Matches Go GenerateScopes."""
|
|
78
|
+
env = os.environ.get("ENV") or os.environ.get("DANA_ENV") or "sandbox"
|
|
79
|
+
if env.lower() != "production":
|
|
80
|
+
return "CASHIER,AGREEMENT_PAY,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA"
|
|
81
|
+
return "MINI_DANA,CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE"
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def generate_timestamp() -> str:
|
|
85
|
+
"""Timestamp in Asia/Jakarta RFC3339 format. Matches Go GenerateTimestamp."""
|
|
86
|
+
try:
|
|
87
|
+
from zoneinfo import ZoneInfo
|
|
88
|
+
|
|
89
|
+
now = datetime.now(ZoneInfo("Asia/Jakarta"))
|
|
90
|
+
except Exception:
|
|
91
|
+
now = datetime.now(timezone.utc) + timedelta(hours=7)
|
|
92
|
+
return now.strftime("%Y-%m-%dT%H:%M:%S+07:00")
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _get_attr(data, name: str, default=None):
|
|
96
|
+
if isinstance(data, dict):
|
|
97
|
+
return data.get(name, default)
|
|
98
|
+
return getattr(data, name, default)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _is_field_set(data, field_name: str) -> bool:
|
|
102
|
+
fields_set = getattr(data, "model_fields_set", None)
|
|
103
|
+
if fields_set is None:
|
|
104
|
+
fields_set = getattr(data, "__fields_set__", None)
|
|
105
|
+
if fields_set is not None:
|
|
106
|
+
return field_name in fields_set
|
|
107
|
+
if isinstance(data, dict):
|
|
108
|
+
return field_name in data
|
|
109
|
+
return getattr(data, field_name, None) is not None
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _normalize_seamless_data(seamless_data) -> Dict[str, Any]:
|
|
113
|
+
if seamless_data is None:
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
if isinstance(seamless_data, dict):
|
|
117
|
+
raw = dict(seamless_data)
|
|
118
|
+
elif hasattr(seamless_data, "model_dump"):
|
|
119
|
+
raw = seamless_data.model_dump(exclude_none=True)
|
|
120
|
+
elif hasattr(seamless_data, "dict"):
|
|
121
|
+
raw = seamless_data.dict(exclude_none=True)
|
|
122
|
+
else:
|
|
123
|
+
try:
|
|
124
|
+
raw = vars(seamless_data)
|
|
125
|
+
except TypeError:
|
|
126
|
+
raw = json.loads(json.dumps(seamless_data))
|
|
127
|
+
|
|
128
|
+
result = {
|
|
129
|
+
key: value
|
|
130
|
+
for key, value in raw.items()
|
|
131
|
+
if value is not None and value != ""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if "mobileNumber" in result:
|
|
135
|
+
result["mobile"] = result.pop("mobileNumber")
|
|
136
|
+
if "mobile_number" in result:
|
|
137
|
+
result["mobile"] = result.pop("mobile_number")
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _build_oauth_query_string(url_params: Dict[str, str]) -> str:
|
|
143
|
+
parts = []
|
|
144
|
+
for key, value in url_params.items():
|
|
145
|
+
if value is None:
|
|
146
|
+
continue
|
|
147
|
+
if key in Util._OAUTH_QUERY_ESCAPE_KEYS:
|
|
148
|
+
parts.append(f"{key}={urllib.parse.quote(str(value), safe='')}")
|
|
149
|
+
else:
|
|
150
|
+
parts.append(f"{key}={value}")
|
|
151
|
+
return "&".join(parts)
|
|
152
|
+
|
|
67
153
|
@staticmethod
|
|
68
154
|
def generate_oauth_url(data, private_key: Optional[str] = None, private_key_path: Optional[str] = None) -> str:
|
|
69
155
|
"""
|
|
70
156
|
Generate OAuth URL for testing
|
|
71
|
-
|
|
157
|
+
|
|
72
158
|
Args:
|
|
73
159
|
data: OAuth URL data object
|
|
74
160
|
private_key: Optional private key content
|
|
75
161
|
private_key_path: Optional path to private key file
|
|
76
|
-
|
|
162
|
+
|
|
77
163
|
Returns:
|
|
78
164
|
str: The generated OAuth URL
|
|
79
165
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Get mode or default to API
|
|
84
|
-
mode = getattr(data, 'mode', None) or Mode.API
|
|
85
|
-
|
|
86
|
-
# Set base URL based on environment and mode
|
|
166
|
+
env = os.environ.get("DANA_ENV") or os.environ.get("ENV") or "sandbox"
|
|
167
|
+
mode = Util._get_attr(data, "mode") or Mode.API
|
|
168
|
+
|
|
87
169
|
if mode == Mode.DEEPLINK:
|
|
88
|
-
base_url =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
base_url = (
|
|
171
|
+
"https://m.dana.id/n/link/binding"
|
|
172
|
+
if env.lower() == "production"
|
|
173
|
+
else "https://m.sandbox.dana.id/n/link/binding"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
base_url = (
|
|
177
|
+
"https://m.dana.id/v1.0/get-auth-code"
|
|
178
|
+
if env.lower() == "production"
|
|
179
|
+
else "https://m.sandbox.dana.id/v1.0/get-auth-code"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
partner_id = os.environ.get("X_PARTNER_ID")
|
|
94
183
|
if not partner_id:
|
|
95
|
-
raise RuntimeError(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
state = getattr(data, 'state', None)
|
|
184
|
+
raise RuntimeError("X_PARTNER_ID is not defined")
|
|
185
|
+
|
|
186
|
+
state = Util._get_attr(data, "state")
|
|
99
187
|
if not state:
|
|
100
188
|
state = str(uuid.uuid4())
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
scopes = ','.join(data.scopes)
|
|
189
|
+
|
|
190
|
+
channel_id = Util.generate_channel_id()
|
|
191
|
+
|
|
192
|
+
scopes = Util._get_attr(data, "scopes")
|
|
193
|
+
if scopes:
|
|
194
|
+
scopes = ",".join(scopes) if isinstance(scopes, (list, tuple)) else scopes
|
|
108
195
|
else:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
scopes = 'MINI_DANA,CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE'
|
|
113
|
-
|
|
114
|
-
# Use provided external ID or generate a UUID
|
|
115
|
-
external_id = getattr(data, 'external_id', None)
|
|
196
|
+
scopes = Util.generate_scopes()
|
|
197
|
+
|
|
198
|
+
external_id = Util._get_attr(data, "external_id")
|
|
116
199
|
if not external_id:
|
|
117
200
|
external_id = str(uuid.uuid4())
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
# Python's timezone handling is different from PHP
|
|
125
|
-
now = datetime.now(timezone(timedelta(hours=7)))
|
|
126
|
-
timestamp = now.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
127
|
-
# Format timezone correctly with a colon
|
|
128
|
-
timestamp = timestamp[:-2] + ':' + timestamp[-2:]
|
|
129
|
-
except Exception:
|
|
130
|
-
# Fallback if timezone calculation fails
|
|
131
|
-
now = datetime.now(timezone.utc)
|
|
132
|
-
now += timedelta(hours=7) # Add 7 hours for UTC+7
|
|
133
|
-
timestamp = now.strftime('%Y-%m-%dT%H:%M:%S+07:00')
|
|
134
|
-
|
|
135
|
-
# Build URL with required parameters
|
|
201
|
+
|
|
202
|
+
merchant_id = Util._get_attr(data, "merchant_id") or os.environ.get("MERCHANT_ID", "")
|
|
203
|
+
timestamp = Util.generate_timestamp()
|
|
204
|
+
request_id = None
|
|
205
|
+
|
|
136
206
|
if mode == Mode.DEEPLINK:
|
|
137
207
|
request_id = str(uuid.uuid4())
|
|
138
208
|
url_params = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
"partnerId": partner_id,
|
|
210
|
+
"scopes": scopes,
|
|
211
|
+
"terminalType": TerminalType.WEB,
|
|
212
|
+
"externalId": external_id,
|
|
213
|
+
"requestId": request_id,
|
|
214
|
+
"redirectUrl": Util._get_attr(data, "redirect_url"),
|
|
215
|
+
"state": state,
|
|
146
216
|
}
|
|
147
|
-
|
|
217
|
+
else:
|
|
148
218
|
url_params = {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
219
|
+
"partnerId": partner_id,
|
|
220
|
+
"scopes": scopes,
|
|
221
|
+
"externalId": external_id,
|
|
222
|
+
"channelId": channel_id,
|
|
223
|
+
"redirectUrl": Util._get_attr(data, "redirect_url"),
|
|
224
|
+
"timestamp": timestamp,
|
|
225
|
+
"state": state,
|
|
226
|
+
"isSnapBI": "true",
|
|
157
227
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
if merchant_id:
|
|
229
|
+
url_params["merchantId"] = merchant_id
|
|
230
|
+
|
|
231
|
+
sub_merchant_id = Util._get_attr(data, "sub_merchant_id")
|
|
232
|
+
if sub_merchant_id:
|
|
233
|
+
url_params["subMerchantId"] = sub_merchant_id
|
|
234
|
+
|
|
235
|
+
if Util._is_field_set(data, "lang"):
|
|
236
|
+
lang = Util._get_attr(data, "lang")
|
|
237
|
+
if lang:
|
|
238
|
+
url_params["lang"] = lang
|
|
239
|
+
|
|
240
|
+
if Util._is_field_set(data, "allow_registration"):
|
|
241
|
+
allow_registration = Util._get_attr(data, "allow_registration")
|
|
242
|
+
if allow_registration is not None:
|
|
243
|
+
url_params["allowRegistration"] = (
|
|
244
|
+
"true" if allow_registration else "false"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
seamless_data = Util._normalize_seamless_data(Util._get_attr(data, "seamless_data"))
|
|
177
248
|
if seamless_data:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
seamless_data['reqTime'] = timestamp
|
|
197
|
-
seamless_data['verifiedTime'] = "0"
|
|
198
|
-
seamless_data['reqMsgId'] = request_id
|
|
199
|
-
|
|
200
|
-
seamless_data_json = json.dumps(seamless_data)
|
|
201
|
-
url_params['seamlessData'] = seamless_data_json
|
|
202
|
-
|
|
203
|
-
url_params['seamlessSign'] = Util.generate_seamless_sign(seamless_data, private_key, private_key_path)
|
|
204
|
-
|
|
205
|
-
# Remove None values
|
|
206
|
-
url_params = {k: v for k, v in url_params.items() if v is not None}
|
|
207
|
-
|
|
208
|
-
# Build the final URL (RFC3986 quote for seamlessData/seamlessSign parity with Go/PHP)
|
|
209
|
-
return base_url + '?' + urllib.parse.urlencode(url_params, quote_via=urllib.parse.quote)
|
|
210
|
-
|
|
249
|
+
if mode == Mode.DEEPLINK and request_id:
|
|
250
|
+
seamless_data["externalUid"] = external_id
|
|
251
|
+
seamless_data["reqTime"] = timestamp
|
|
252
|
+
seamless_data["verifiedTime"] = "0"
|
|
253
|
+
seamless_data["reqMsgId"] = request_id
|
|
254
|
+
|
|
255
|
+
seamless_data_json = json.dumps(seamless_data, separators=(",", ":"))
|
|
256
|
+
url_params["seamlessData"] = seamless_data_json
|
|
257
|
+
|
|
258
|
+
pk = private_key or os.environ.get("PRIVATE_KEY")
|
|
259
|
+
pk_path = private_key_path or os.environ.get("PRIVATE_KEY_PATH")
|
|
260
|
+
seamless_sign = Util.generate_seamless_sign(seamless_data, pk, pk_path)
|
|
261
|
+
if seamless_sign:
|
|
262
|
+
url_params["seamlessSign"] = seamless_sign
|
|
263
|
+
|
|
264
|
+
query_string = Util._build_oauth_query_string(url_params)
|
|
265
|
+
return f"{base_url}?{query_string}"
|
|
266
|
+
|
|
211
267
|
@staticmethod
|
|
212
268
|
def generate_seamless_sign(seamless_data: Dict[str, Any], private_key: Optional[str] = None, private_key_path: Optional[str] = None) -> str:
|
|
213
269
|
"""
|
|
214
270
|
Generate seamless sign for OAuth URL
|
|
215
|
-
|
|
271
|
+
|
|
216
272
|
Args:
|
|
217
273
|
seamless_data: The seamless data to sign
|
|
218
274
|
private_key: Optional private key content
|
|
219
275
|
private_key_path: Optional path to private key file
|
|
220
|
-
|
|
276
|
+
|
|
221
277
|
Returns:
|
|
222
278
|
str: The generated signature
|
|
223
279
|
"""
|
|
224
280
|
try:
|
|
225
|
-
# Get properly formatted private key using SnapHeader utility method
|
|
226
281
|
usable_private_key = SnapHeader.get_usable_private_key(private_key, private_key_path)
|
|
227
|
-
data_to_sign = json.dumps(seamless_data).encode()
|
|
228
|
-
|
|
229
|
-
# Use cryptography library if available
|
|
282
|
+
data_to_sign = json.dumps(seamless_data, separators=(",", ":")).encode()
|
|
283
|
+
|
|
230
284
|
if CRYPTOGRAPHY_AVAILABLE:
|
|
231
|
-
# Load the private key
|
|
232
285
|
key = load_pem_private_key(usable_private_key.encode(), password=None)
|
|
233
|
-
|
|
234
|
-
# Sign the data
|
|
235
286
|
signature = key.sign(
|
|
236
287
|
data_to_sign,
|
|
237
288
|
padding.PKCS1v15(),
|
|
238
|
-
hashes.SHA256()
|
|
289
|
+
hashes.SHA256(),
|
|
239
290
|
)
|
|
240
|
-
|
|
241
|
-
# Return base64 encoded signature
|
|
242
291
|
return base64.b64encode(signature).decode()
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
292
|
+
|
|
293
|
+
return hashlib.sha256(data_to_sign + str(int(datetime.now().timestamp())).encode()).hexdigest()
|
|
294
|
+
|
|
295
|
+
except Exception:
|
|
296
|
+
return hashlib.sha256(
|
|
297
|
+
(json.dumps(seamless_data, separators=(",", ":")) + str(int(datetime.now().timestamp()))).encode()
|
|
298
|
+
).hexdigest()
|
|
299
|
+
|
|
251
300
|
@staticmethod
|
|
252
301
|
def generate_complete_payment_url(widget_payment_response: WidgetPaymentResponse = None, apply_ott_response: ApplyOTTResponse = None) -> str:
|
|
253
302
|
"""
|
|
254
303
|
Combines the webRedirectUrl from WidgetPaymentResponse with the OTT token from ApplyOTTResponse
|
|
255
|
-
|
|
304
|
+
|
|
256
305
|
Args:
|
|
257
306
|
widget_payment_response: The widget payment response
|
|
258
307
|
apply_ott_response: The apply OTT response
|
|
259
|
-
|
|
308
|
+
|
|
260
309
|
Returns:
|
|
261
310
|
str: The generated payment URL or empty string if inputs are invalid
|
|
262
311
|
"""
|
|
263
312
|
if widget_payment_response is None or apply_ott_response is None:
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
web_redirect_url = getattr(widget_payment_response,
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
web_redirect_url = getattr(widget_payment_response, "web_redirect_url", None)
|
|
267
316
|
if not web_redirect_url:
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
user_resources = getattr(apply_ott_response,
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
user_resources = getattr(apply_ott_response, "user_resources", None)
|
|
271
320
|
if not user_resources or len(user_resources) == 0:
|
|
272
321
|
return web_redirect_url
|
|
273
|
-
|
|
274
|
-
ott_value = getattr(user_resources[0],
|
|
322
|
+
|
|
323
|
+
ott_value = getattr(user_resources[0], "value", None) if user_resources else None
|
|
275
324
|
if not ott_value:
|
|
276
325
|
return web_redirect_url
|
|
277
326
|
|
|
278
327
|
parsed = urllib.parse.urlparse(web_redirect_url)
|
|
279
328
|
q = dict(urllib.parse.parse_qsl(parsed.query, keep_blank_values=True))
|
|
280
|
-
q[
|
|
329
|
+
q["ott"] = str(ott_value)
|
|
281
330
|
new_query = urllib.parse.urlencode(q)
|
|
282
331
|
return urllib.parse.urlunparse((
|
|
283
332
|
parsed.scheme,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dana-python
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: API Client (SDK) for DANA APIs based on https://dashboard.dana.id/api-docs
|
|
5
5
|
Author-email: DANA Package Manager <package-manager@dana.id>
|
|
6
6
|
Maintainer-email: DANA Package Manager <package-manager@dana.id>
|
|
@@ -9,10 +9,10 @@ dana/base/model.py,sha256=8-wXAQ5OFjJRTCDcZIiYQp99ym2irfRCsL325yinDUc,1241
|
|
|
9
9
|
dana/base/types.py,sha256=JjQip4RXwk80bKREbFyJanv23C795XWzjRJD2fOqlUM,719
|
|
10
10
|
dana/disbursement/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
11
11
|
dana/disbursement/v1/__init__.py,sha256=C981Ksji-mo9bOhFhI-9g0upO279XZ1sn8a6GIHkU-k,2983
|
|
12
|
-
dana/disbursement/v1/custom_validation.py,sha256=
|
|
12
|
+
dana/disbursement/v1/custom_validation.py,sha256=oOthimF_5IgC2MVRxd7gmxzGcNTUHd4S1pKJDIIwAA0,15566
|
|
13
13
|
dana/disbursement/v1/enum.py,sha256=iPEFAIsblr4K2d0pzjxxuXhvJOnZd9nzfyyiCLCCC2o,1054
|
|
14
14
|
dana/disbursement/v1/api/__init__.py,sha256=g22N5qjTc2jl1wsbCydP1g7cqWBthEaOB5g4SWlp6_c,711
|
|
15
|
-
dana/disbursement/v1/api/disbursement_api.py,sha256=
|
|
15
|
+
dana/disbursement/v1/api/disbursement_api.py,sha256=hUd1Rk-huOPNglE5cuJh7iwwHi9i4askCAl75UYNCw8,82240
|
|
16
16
|
dana/disbursement/v1/models/__init__.py,sha256=eCiQWHiKPdQvcbAz7kclRMGoHL4qMIm2DPakBtGxVm4,2858
|
|
17
17
|
dana/disbursement/v1/models/bank_account_inquiry_request.py,sha256=-qK599Biioyhn-RMUxq02hnzT2qkYOGEuFLDxIaOfJk,5431
|
|
18
18
|
dana/disbursement/v1/models/bank_account_inquiry_request_additional_info.py,sha256=keMtkHP9VcfRRtFBh39_1b3h2tyxd1LGRupZw-RupW4,6468
|
|
@@ -103,7 +103,7 @@ dana/merchant_management/v1/models/update_shop_response_response_head.py,sha256=
|
|
|
103
103
|
dana/merchant_management/v1/models/user_name.py,sha256=eRNJu5iVsMZZdU7nA9QA1g_4RV-bxxGy4MGIBNbXevs,4366
|
|
104
104
|
dana/payment_gateway/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
105
105
|
dana/payment_gateway/v1/__init__.py,sha256=cJRNM80F042f3ZsUXeP5t9wTajH5NDBbpnXSV5y_k4E,4243
|
|
106
|
-
dana/payment_gateway/v1/custom_validation.py,sha256=
|
|
106
|
+
dana/payment_gateway/v1/custom_validation.py,sha256=OyT-ulsQuNuHwSLleDXv_o64nxzfFREdaeCJiFNtZkE,25753
|
|
107
107
|
dana/payment_gateway/v1/enum.py,sha256=8vLoW-Ke4Aui_Iexgvh5c92Tw-23LIuMwg6jtfBCMAY,2687
|
|
108
108
|
dana/payment_gateway/v1/api/__init__.py,sha256=xYZF0wAVxZ7tn-36ofOKJWEJzc-u4ixG8-gKbGK2Bng,942
|
|
109
109
|
dana/payment_gateway/v1/api/payment_gateway_api.py,sha256=M5MMDguPK0fNy8lSlpvH07aEioj9IFLm9zOOAo8eaBQ,69188
|
|
@@ -168,7 +168,7 @@ dana/widget/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
|
168
168
|
dana/widget/v1/__init__.py,sha256=ibJjQQ3NDAK_UwYZKHZzBVAScX4mkQixi_LD3RGTr4U,5576
|
|
169
169
|
dana/widget/v1/custom_validation.py,sha256=XqQLJu3Dh5XdUpG5eXXSq8dYVymha3vDdpkoKjJ0E7U,7028
|
|
170
170
|
dana/widget/v1/enum.py,sha256=-DS06ZaPG_feTR57fy8hzWeFwPAPsunXvqfisfT7HlI,3507
|
|
171
|
-
dana/widget/v1/util.py,sha256=
|
|
171
|
+
dana/widget/v1/util.py,sha256=iMSDdUaLvq0QnjepP_Gor04bO_3HddhxHCyhdjwJ4yA,12588
|
|
172
172
|
dana/widget/v1/api/__init__.py,sha256=0M_z47tKg4vjaHIDSSE7f9COYZd1KmxVl40ZMi1kjog,922
|
|
173
173
|
dana/widget/v1/api/widget_api.py,sha256=WU1Z8-Xo-5oT_PXA5SAJm1pZnYkfKV-VfYNRB_QXp8k,114510
|
|
174
174
|
dana/widget/v1/models/__init__.py,sha256=WpNeouhhZ_NSgCPSnmQ5YYHnQnO7Vo-OKD7gT9gon3s,5469
|
|
@@ -229,8 +229,8 @@ dana/widget/v1/models/virtual_account_info.py,sha256=qCFyTqwOFY0KwawiQhRfWYwv0Hv
|
|
|
229
229
|
dana/widget/v1/models/widget_payment_request.py,sha256=KZPOauNqtDvtETLSAgkfgLagOiX2H5KaOGroSbRKCAg,8469
|
|
230
230
|
dana/widget/v1/models/widget_payment_request_additional_info.py,sha256=Qa4Akl-KxeBdh3ITnb0_LxVmgZA_YJsYgW_MHKTHj1A,6273
|
|
231
231
|
dana/widget/v1/models/widget_payment_response.py,sha256=KNfxpyOeNjyRG8aNRzTCaY1W5i0PgSVj3x2O8I6b_kE,6037
|
|
232
|
-
dana_python-2.2.
|
|
233
|
-
dana_python-2.2.
|
|
234
|
-
dana_python-2.2.
|
|
235
|
-
dana_python-2.2.
|
|
236
|
-
dana_python-2.2.
|
|
232
|
+
dana_python-2.2.2.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
|
|
233
|
+
dana_python-2.2.2.dist-info/METADATA,sha256=O2b7-OHIQYqNUTk407-NKW3GqE4v29lWOd_nG6eq3hY,5568
|
|
234
|
+
dana_python-2.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
235
|
+
dana_python-2.2.2.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
|
|
236
|
+
dana_python-2.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|