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.
@@ -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
- enrich_transfer_to_dana_error,
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
- if 'bank_account_inquiry' == 'transfer_to_dana':
148
- try:
149
- raise enrich_transfer_to_dana_error(bank_account_inquiry_request, e)
150
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'bank_account_inquiry' == 'transfer_to_dana':
228
- try:
229
- raise enrich_transfer_to_dana_error(bank_account_inquiry_request, e)
230
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'dana_account_inquiry' == 'transfer_to_dana':
468
- try:
469
- raise enrich_transfer_to_dana_error(dana_account_inquiry_request, e)
470
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'dana_account_inquiry' == 'transfer_to_dana':
548
- try:
549
- raise enrich_transfer_to_dana_error(dana_account_inquiry_request, e)
550
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_bank' == 'transfer_to_dana':
789
- try:
790
- raise enrich_transfer_to_dana_error(transfer_to_bank_request, e)
791
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_bank' == 'transfer_to_dana':
870
- try:
871
- raise enrich_transfer_to_dana_error(transfer_to_bank_request, e)
872
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_bank_inquiry_status' == 'transfer_to_dana':
1111
- try:
1112
- raise enrich_transfer_to_dana_error(transfer_to_bank_inquiry_status_request, e)
1113
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_bank_inquiry_status' == 'transfer_to_dana':
1191
- try:
1192
- raise enrich_transfer_to_dana_error(transfer_to_bank_inquiry_status_request, e)
1193
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_dana' == 'transfer_to_dana':
1431
- try:
1432
- raise enrich_transfer_to_dana_error(transfer_to_dana_request, e)
1433
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_dana' == 'transfer_to_dana':
1511
- try:
1512
- raise enrich_transfer_to_dana_error(transfer_to_dana_request, e)
1513
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_dana_inquiry_status' == 'transfer_to_dana':
1751
- try:
1752
- raise enrich_transfer_to_dana_error(transfer_to_dana_inquiry_status_request, e)
1753
- except (ImportError, ModuleNotFoundError, NameError):
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
- if 'transfer_to_dana_inquiry_status' == 'transfer_to_dana':
1831
- try:
1832
- raise enrich_transfer_to_dana_error(transfer_to_dana_inquiry_status_request, e)
1833
- except (ImportError, ModuleNotFoundError, NameError):
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 (Go-compatible). Uses markers to avoid duplicates. No 150-char truncate."""
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 _is_business_error_response(response_code: Any) -> bool:
79
- code = str(response_code or '').strip()
80
- return code == '' or not code.startswith('200')
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 custom_validation_response(request: Any, response: Any) -> None:
383
- """Augment TransferToDana responses in sandbox on business errors."""
384
- if not _is_sandbox() or request is None or response is None:
385
- return
386
- if request.__class__.__name__ != 'TransferToDanaRequest':
387
- return
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
- if not _is_business_error_response(response_code):
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.response_message = _append_sandbox_hint(
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 enrich_transfer_to_dana_error(request: Any, exc: ApiException) -> ApiException:
402
- """Enrich TransferToDana HTTP errors in sandbox (keeps exception; updates data)."""
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=str(payload.get('partnerReferenceNo') or ''),
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=exc.reason,
442
- body=exc.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
- SANDBOX_SUB_MERCHANT_ID_GUIDANCE_HINT = (
64
- 'Make sure your subMerchantId exists. You can see it at '
65
- 'https://dashboard.dana.id/sandbox/submerchants in the External Division ID section.'
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 (QRIS / subMerchantId guidance)."""
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
- custom_validation_response(request, response)
636
+ original_msg = response.response_message
637
+ _apply_sandbox_create_order_hints(request, response)
588
638
 
589
639
  reason = exc.reason
590
- # ERROR QRIS hint on exception reason for Redirect without externalStoreId (Go puts on err string)
591
- if request.__class__.__name__ == 'CreateOrderByRedirectRequest':
592
- external_store_id = getattr(request, 'external_store_id', None)
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=exc.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, List, Union
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
- # Use environment variable or default to sandbox
81
- env = os.environ.get('DANA_ENV') or os.environ.get('ENV') or 'sandbox'
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 = 'https://m.dana.id/n/link/binding' if env.lower() == 'production' else 'https://m.sandbox.dana.id/n/link/binding'
89
- elif mode == Mode.API:
90
- base_url = 'https://m.dana.id/v1.0/get-auth-code' if env.lower() == 'production' else 'https://m.sandbox.dana.id/v1.0/get-auth-code'
91
-
92
- # Get partner ID from environment
93
- partner_id = os.environ.get('X_PARTNER_ID')
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('X_PARTNER_ID is not defined')
96
-
97
- # Use provided state or generate a new UUID
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
- # Generate channel ID
103
- channel_id = os.environ.get('X_PARTNER_ID')
104
-
105
- # Get scopes based on environment
106
- if hasattr(data, 'scopes') and data.scopes:
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
- if env.lower() != 'production':
110
- scopes = 'CASHIER,AGREEMENT_PAY,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA'
111
- else:
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
- # Get merchant ID
120
- merchant_id = getattr(data, 'merchant_id', None)
121
-
122
- # Generate timestamp in Jakarta time (UTC+7) with RFC3339 format
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
- 'partnerId': partner_id,
140
- 'scopes': scopes,
141
- 'terminalType': TerminalType.WEB,
142
- 'externalId': external_id,
143
- 'requestId': request_id,
144
- 'redirectUrl': data.redirect_url if hasattr(data, 'redirect_url') else None,
145
- 'state': state,
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
- elif mode == Mode.API:
217
+ else:
148
218
  url_params = {
149
- 'partnerId': partner_id,
150
- 'scopes': scopes,
151
- 'externalId': external_id,
152
- 'channelId': channel_id,
153
- 'redirectUrl': data.redirect_url if hasattr(data, 'redirect_url') else None,
154
- 'timestamp': timestamp,
155
- 'state': state,
156
- 'isSnapBI': 'true'
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
- # Add merchant ID if provided
160
- if merchant_id and mode == Mode.API:
161
- url_params['merchantId'] = merchant_id
162
-
163
- # Add subMerchantId if provided
164
- if hasattr(data, 'sub_merchant_id') and data.sub_merchant_id and mode == Mode.API:
165
- url_params['subMerchantId'] = data.sub_merchant_id
166
-
167
- # Add lang if provided
168
- if hasattr(data, 'lang') and data.lang and mode == Mode.API:
169
- url_params['lang'] = data.lang
170
-
171
- # Add allowRegistration if provided
172
- if hasattr(data, 'allow_registration') and data.allow_registration is not None and mode == Mode.API:
173
- url_params['allowRegistration'] = 'true' if data.allow_registration else 'false'
174
-
175
- # Handle seamless data if provided
176
- seamless_data = getattr(data, 'seamless_data', None)
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
- # Convert object to dict if necessary
179
- if not isinstance(seamless_data, dict):
180
- try:
181
- seamless_data = vars(seamless_data)
182
- except TypeError:
183
- seamless_data = json.loads(json.dumps(seamless_data))
184
-
185
- # Ensure seamless_data is a dict and convert mobileNumber/mobile_number to mobile if needed
186
- if isinstance(seamless_data, dict):
187
- if 'mobileNumber' in seamless_data:
188
- seamless_data['mobile'] = seamless_data['mobileNumber']
189
- del seamless_data['mobileNumber']
190
- if 'mobile_number' in seamless_data:
191
- seamless_data['mobile'] = seamless_data['mobile_number']
192
- del seamless_data['mobile_number']
193
-
194
- if mode == Mode.DEEPLINK:
195
- seamless_data['externalUid'] = getattr(data, 'external_id', '')
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
- else:
244
- # Fallback to a simple hash for testing
245
- return hashlib.sha256(data_to_sign + str(int(datetime.now().timestamp())).encode()).hexdigest()
246
-
247
- except Exception as e:
248
- # For testing purposes, generate a mock signature if key is not available
249
- return hashlib.sha256((json.dumps(seamless_data) + str(int(datetime.now().timestamp()))).encode()).hexdigest()
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, 'web_redirect_url', None)
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, 'user_resources', None)
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], 'value', None) if user_resources else None
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['ott'] = str(ott_value)
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.0
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=P2SALz8jrY2rxO_xq53ELqnOGseD3P5CX9tn1iucMfo,16315
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=Mx8wA5XPAgJ2HCq_nDEfTATOCuS9yvK0RWfnL-HQxLM,83244
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=ON80ATI0WidU3ZnV7xmWRkQ2S-EYVX2ungmj99SsnsE,23924
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=4hRFry5ROrhahyQ7hlHNj7TT-lS_-CMH750Ed4jVQLw,11957
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.0.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
233
- dana_python-2.2.0.dist-info/METADATA,sha256=tGh_wdLw2fodREfTopwuy0R0aPOojHkbxHn9I6p6R_M,5568
234
- dana_python-2.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
235
- dana_python-2.2.0.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
236
- dana_python-2.2.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5