dana-python 2.1.8__tar.gz → 2.1.9__tar.gz

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.
Files changed (249) hide show
  1. {dana_python-2.1.8/dana_python.egg-info → dana_python-2.1.9}/PKG-INFO +1 -1
  2. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/shop_info.py +1 -1
  3. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/webhook.py +173 -45
  4. {dana_python-2.1.8 → dana_python-2.1.9/dana_python.egg-info}/PKG-INFO +1 -1
  5. {dana_python-2.1.8 → dana_python-2.1.9}/pyproject.toml +1 -1
  6. dana_python-2.1.9/tests/test_webhook.py +275 -0
  7. dana_python-2.1.8/tests/test_webhook.py +0 -111
  8. {dana_python-2.1.8 → dana_python-2.1.9}/LICENSE +0 -0
  9. {dana_python-2.1.8 → dana_python-2.1.9}/README.md +0 -0
  10. {dana_python-2.1.8 → dana_python-2.1.9}/dana/__init__.py +0 -0
  11. {dana_python-2.1.8 → dana_python-2.1.9}/dana/api_client.py +0 -0
  12. {dana_python-2.1.8 → dana_python-2.1.9}/dana/api_response.py +0 -0
  13. {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/__init__.py +0 -0
  14. {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/configuration.py +0 -0
  15. {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/model.py +0 -0
  16. {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/types.py +0 -0
  17. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/__init__.py +0 -0
  18. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/__init__.py +0 -0
  19. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/api/__init__.py +0 -0
  20. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/api/disbursement_api.py +0 -0
  21. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/enum.py +0 -0
  22. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/__init__.py +0 -0
  23. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_request.py +0 -0
  24. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_request_additional_info.py +0 -0
  25. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_response.py +0 -0
  26. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_response_additional_info.py +0 -0
  27. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_request.py +0 -0
  28. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_request_additional_info.py +0 -0
  29. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_response.py +0 -0
  30. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/money.py +0 -0
  31. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_inquiry_status_request.py +0 -0
  32. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_inquiry_status_response.py +0 -0
  33. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_request.py +0 -0
  34. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_request_additional_info.py +0 -0
  35. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_response.py +0 -0
  36. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_inquiry_status_request.py +0 -0
  37. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_inquiry_status_response.py +0 -0
  38. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_request.py +0 -0
  39. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_request_additional_info.py +0 -0
  40. {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_response.py +0 -0
  41. {dana_python-2.1.8 → dana_python-2.1.9}/dana/exceptions.py +0 -0
  42. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/__init__.py +0 -0
  43. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/__init__.py +0 -0
  44. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/api/__init__.py +0 -0
  45. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/api/merchant_management_api.py +0 -0
  46. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/enum.py +0 -0
  47. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/__init__.py +0 -0
  48. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/address_info.py +0 -0
  49. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/asset_card_list_item.py +0 -0
  50. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/business_docs.py +0 -0
  51. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_request.py +0 -0
  52. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_request_ext_info.py +0 -0
  53. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response.py +0 -0
  54. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response.py +0 -0
  55. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response_body.py +0 -0
  56. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response_head.py +0 -0
  57. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_request.py +0 -0
  58. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response.py +0 -0
  59. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response.py +0 -0
  60. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response_body.py +0 -0
  61. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response_head.py +0 -0
  62. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/division_resource_info.py +0 -0
  63. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/member_asset_result_info.py +0 -0
  64. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_account_info.py +0 -0
  65. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_certificate_info.py +0 -0
  66. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_address.py +0 -0
  67. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_email.py +0 -0
  68. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_mobile_no.py +0 -0
  69. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_corporate_certificate.py +0 -0
  70. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_information.py +0 -0
  71. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_resource_information.py +0 -0
  72. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/mobile_no_info.py +0 -0
  73. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/pic_info.py +0 -0
  74. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_request.py +0 -0
  75. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response.py +0 -0
  76. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response_response.py +0 -0
  77. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response_response_body.py +0 -0
  78. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response_response_head.py +0 -0
  79. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_request.py +0 -0
  80. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response.py +0 -0
  81. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response.py +0 -0
  82. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response_body.py +0 -0
  83. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response_head.py +0 -0
  84. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_request.py +0 -0
  85. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response.py +0 -0
  86. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response.py +0 -0
  87. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response_body.py +0 -0
  88. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response_head.py +0 -0
  89. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_request.py +0 -0
  90. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response.py +0 -0
  91. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response.py +0 -0
  92. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response_body.py +0 -0
  93. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response_head.py +0 -0
  94. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_request.py +0 -0
  95. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response.py +0 -0
  96. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response.py +0 -0
  97. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response_body.py +0 -0
  98. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response_head.py +0 -0
  99. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/result_info.py +0 -0
  100. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/shop_resource_info.py +0 -0
  101. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_request.py +0 -0
  102. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response.py +0 -0
  103. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response.py +0 -0
  104. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response_body.py +0 -0
  105. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response_head.py +0 -0
  106. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_request.py +0 -0
  107. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response.py +0 -0
  108. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response_response.py +0 -0
  109. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response_response_head.py +0 -0
  110. {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/user_name.py +0 -0
  111. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/__init__.py +0 -0
  112. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/__init__.py +0 -0
  113. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/api/__init__.py +0 -0
  114. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/api/payment_gateway_api.py +0 -0
  115. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/custom_validation.py +0 -0
  116. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/enum.py +0 -0
  117. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/__init__.py +0 -0
  118. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/actor_context.py +0 -0
  119. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/amount_detail.py +0 -0
  120. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/audit_info.py +0 -0
  121. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/buyer.py +0 -0
  122. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/cancel_order_request.py +0 -0
  123. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/cancel_order_response.py +0 -0
  124. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_payment_info.py +0 -0
  125. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_request.py +0 -0
  126. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_request_additional_info.py +0 -0
  127. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_response.py +0 -0
  128. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_api_additional_info.py +0 -0
  129. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_api_request.py +0 -0
  130. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_redirect_additional_info.py +0 -0
  131. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_redirect_request.py +0 -0
  132. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_response.py +0 -0
  133. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_response_additional_info.py +0 -0
  134. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/env_info.py +0 -0
  135. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/goods.py +0 -0
  136. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/money.py +0 -0
  137. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/order_api_object.py +0 -0
  138. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/order_redirect_object.py +0 -0
  139. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_additional_info.py +0 -0
  140. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_detail.py +0 -0
  141. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_info.py +0 -0
  142. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/payment_view.py +0 -0
  143. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/promo_info.py +0 -0
  144. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_request.py +0 -0
  145. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_response.py +0 -0
  146. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_response_additional_info.py +0 -0
  147. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_option_bill.py +0 -0
  148. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_request.py +0 -0
  149. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_request_additional_info.py +0 -0
  150. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_response.py +0 -0
  151. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/seller.py +0 -0
  152. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/shipping_info.py +0 -0
  153. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/status_detail.py +0 -0
  154. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/time_detail.py +0 -0
  155. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/url_param.py +0 -0
  156. {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/virtual_account_info.py +0 -0
  157. {dana_python-2.1.8 → dana_python-2.1.9}/dana/rest.py +0 -0
  158. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/date_validation.py +0 -0
  159. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/models.py +0 -0
  160. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/open_api_configuration.py +0 -0
  161. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/open_api_header.py +0 -0
  162. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/script.py +0 -0
  163. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/snap_configuration.py +0 -0
  164. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/snap_header.py +0 -0
  165. {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/url.py +0 -0
  166. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/__init__.py +0 -0
  167. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_payment_info.py +0 -0
  168. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_request.py +0 -0
  169. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_request_additional_info.py +0 -0
  170. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_response.py +0 -0
  171. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/money.py +0 -0
  172. {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/pay_option_info.py +0 -0
  173. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/__init__.py +0 -0
  174. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/__init__.py +0 -0
  175. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/api/__init__.py +0 -0
  176. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/api/widget_api.py +0 -0
  177. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/custom_validation.py +0 -0
  178. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/enum.py +0 -0
  179. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/__init__.py +0 -0
  180. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_info.py +0 -0
  181. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_request.py +0 -0
  182. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_request_additional_info.py +0 -0
  183. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_response.py +0 -0
  184. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/amount_detail.py +0 -0
  185. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_request.py +0 -0
  186. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_request_additional_info.py +0 -0
  187. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_response.py +0 -0
  188. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_response_user_resources_inner.py +0 -0
  189. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_authorization_code_request.py +0 -0
  190. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_refresh_token_request.py +0 -0
  191. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response.py +0 -0
  192. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response_additional_info.py +0 -0
  193. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response_additional_info_user_info.py +0 -0
  194. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_request.py +0 -0
  195. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_request_additional_info.py +0 -0
  196. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_response.py +0 -0
  197. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/buyer.py +0 -0
  198. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/cancel_order_request.py +0 -0
  199. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/cancel_order_response.py +0 -0
  200. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/env_info.py +0 -0
  201. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/goods.py +0 -0
  202. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/international_order_info.py +0 -0
  203. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/international_order_info_exchange_rate.py +0 -0
  204. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/money.py +0 -0
  205. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/oauth2_url_data.py +0 -0
  206. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/oauth2_url_data_seamless_data.py +0 -0
  207. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/order.py +0 -0
  208. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_detail.py +0 -0
  209. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_detail_additional_info.py +0 -0
  210. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_info.py +0 -0
  211. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/payment_promo_info.py +0 -0
  212. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/payment_view.py +0 -0
  213. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_request.py +0 -0
  214. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_response.py +0 -0
  215. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_response_additional_info.py +0 -0
  216. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_request.py +0 -0
  217. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response.py +0 -0
  218. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response.py +0 -0
  219. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response_body.py +0 -0
  220. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response_head.py +0 -0
  221. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_request.py +0 -0
  222. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_request_additional_info.py +0 -0
  223. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_response.py +0 -0
  224. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_promo_info.py +0 -0
  225. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/result_info.py +0 -0
  226. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/seller.py +0 -0
  227. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/service_info.py +0 -0
  228. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/shipping_info.py +0 -0
  229. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/status_detail.py +0 -0
  230. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/time_detail.py +0 -0
  231. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/url_param.py +0 -0
  232. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/user_resource_info.py +0 -0
  233. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/virtual_account_info.py +0 -0
  234. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_request.py +0 -0
  235. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_request_additional_info.py +0 -0
  236. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_response.py +0 -0
  237. {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/util.py +0 -0
  238. {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/SOURCES.txt +0 -0
  239. {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/dependency_links.txt +0 -0
  240. {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/requires.txt +0 -0
  241. {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/top_level.txt +0 -0
  242. {dana_python-2.1.8 → dana_python-2.1.9}/setup.cfg +0 -0
  243. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_disbursement_api.py +0 -0
  244. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_merchant_management_api.py +0 -0
  245. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_payment_gateway_api.py +0 -0
  246. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_payment_gateway_with_automation.py +0 -0
  247. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_snap_header.py +0 -0
  248. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_widget_api.py +0 -0
  249. {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_widget_with_automation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dana-python
3
- Version: 2.1.8
3
+ Version: 2.1.9
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>
@@ -36,7 +36,7 @@ class ShopInfo(BaseModel, BaseSdkModel):
36
36
  shop_id: Optional[Annotated[str, Field(strict=True, max_length=64)]] = Field(default=None, description="Information of shop identifier. Required if externalShopId is blank")
37
37
  external_shop_id: Optional[Annotated[str, Field(strict=True, max_length=64)]] = Field(default=None, description="Information of external shop identifier. Required if shopId is blank")
38
38
  operator_id: Optional[Annotated[str, Field(strict=True, max_length=32)]] = Field(default=None, description="Information of operator identifier")
39
- shop_address: Optional[Annotated[str, Field(strict=True, max_length=256)]] = Field(default=None, description="Information of shop address")
39
+ shop_address: Optional[str] = Field(default=None, description="Information of shop address")
40
40
  division_id: Optional[Annotated[str, Field(strict=True, max_length=64)]] = Field(default=None, description="Information of division identifier")
41
41
  external_division_id: Optional[Annotated[str, Field(strict=True, max_length=64)]] = Field(default=None, description="Information of external division identifier")
42
42
  division_type: Optional[Annotated[str, Field(strict=True, max_length=32)]] = Field(default=None, description="Information of division type")
@@ -141,6 +141,27 @@ class WebhookParser:
141
141
  except Exception as e:
142
142
  return "", Exception(f"MinifyJSON: failed to marshal JSON for minification: {e}")
143
143
 
144
+ @staticmethod
145
+ def _has_triple_escaped_json_string_field(json_str: str) -> bool:
146
+ return '":"{\\\\\\"' in json_str
147
+
148
+ @staticmethod
149
+ def _process_over_escaped_minified_json(json_str: str) -> str:
150
+ normalized = json_str.replace('\\\\"', '"')
151
+ return WebhookParser._process_nested_json_fields(normalized)
152
+
153
+ @staticmethod
154
+ def _process_nested_json_fields(json_str: str) -> str:
155
+ normalized_str = json_str.replace('\\\\"', '\\"')
156
+
157
+ def replace_func(match):
158
+ field_name = match.group(1)
159
+ json_value = match.group(2)
160
+ escaped_value = json_value.replace('"', '\\"')
161
+ return f'"{field_name}":"{escaped_value}"'
162
+
163
+ return re.sub(r'"(\w+)":"(\{.*?\})"', replace_func, normalized_str)
164
+
144
165
  @staticmethod
145
166
  def _ensure_minified_json(json_str: str) -> tuple[str, Exception]:
146
167
  """
@@ -148,27 +169,109 @@ class WebhookParser:
148
169
  Returns tuple of (minified_json, error)
149
170
  """
150
171
  try:
151
- normalized_str = json_str.replace('\\"', '"')
152
-
153
- pattern = r'"(\w+)":"(\{.*?\})"'
154
- def replace_func(match):
155
- field_name = match.group(1)
156
- json_value = match.group(2)
157
- escaped_value = json_value.replace('"', '\\"')
158
- return f'"{field_name}":"{escaped_value}"'
159
-
160
- processed_str = re.sub(pattern, replace_func, normalized_str)
161
-
172
+ if WebhookParser._is_json_minified(json_str) and not WebhookParser._has_triple_escaped_json_string_field(json_str):
173
+ return json_str, None
174
+
175
+ if WebhookParser._is_json_minified(json_str):
176
+ return WebhookParser._process_over_escaped_minified_json(json_str), None
177
+
178
+ normalized_str = json_str.replace('\\\\"', '\\"')
179
+ processed_str = WebhookParser._process_nested_json_fields(normalized_str)
180
+
162
181
  if WebhookParser._is_json_minified(processed_str):
163
182
  return processed_str, None
164
-
183
+
165
184
  return WebhookParser._minify_json(processed_str)
166
-
185
+
167
186
  except json.JSONDecodeError as e:
168
187
  return "", Exception(f"EnsureMinifiedJSON: failed to unmarshal JSON: {e}")
169
188
  except Exception as e:
170
189
  return "", Exception(f"EnsureMinifiedJSON: failed to marshal JSON for minification: {e}")
171
190
 
191
+ @staticmethod
192
+ def _is_valid_json(json_str: str) -> bool:
193
+ try:
194
+ json.loads(json_str)
195
+ return True
196
+ except json.JSONDecodeError:
197
+ return False
198
+
199
+ @staticmethod
200
+ def _collapse_triple_backslash_quotes(s: str) -> str:
201
+ if '\\\\\\"' not in s:
202
+ return s
203
+ return s.replace('\\\\\\"', '\\"')
204
+
205
+ @staticmethod
206
+ def _collapse_double_backslash_quotes(s: str) -> str:
207
+ if '\\\\"' not in s:
208
+ return s
209
+ return s.replace('\\\\"', '"')
210
+
211
+ @staticmethod
212
+ def _remove_colon_space_before_quoted_value(s: str) -> str:
213
+ if ': \\"' not in s:
214
+ return s
215
+ return s.replace(': \\"', ':\\"')
216
+
217
+ @staticmethod
218
+ def _normalize_over_escaped_quotes(s: str) -> str:
219
+ if '\\\\"' in s:
220
+ return s.replace('\\\\"', '\\"')
221
+ return s
222
+
223
+ @staticmethod
224
+ def _body_forms_for_signature(request_body: str) -> list[str]:
225
+ seen: set[str] = set()
226
+ forms: list[str] = []
227
+
228
+ def add(form: str) -> None:
229
+ if form and form not in seen:
230
+ seen.add(form)
231
+ forms.append(form)
232
+
233
+ collapsed = WebhookParser._collapse_triple_backslash_quotes(request_body)
234
+ if collapsed != request_body and WebhookParser._is_valid_json(collapsed):
235
+ add(collapsed)
236
+
237
+ collapsed_spaced = WebhookParser._remove_colon_space_before_quoted_value(collapsed)
238
+ if collapsed_spaced != request_body and WebhookParser._is_valid_json(collapsed_spaced):
239
+ add(collapsed_spaced)
240
+
241
+ collapsed = WebhookParser._collapse_double_backslash_quotes(request_body)
242
+ if collapsed != request_body and WebhookParser._is_valid_json(collapsed):
243
+ add(collapsed)
244
+
245
+ spaced = WebhookParser._remove_colon_space_before_quoted_value(request_body)
246
+ if spaced != request_body and WebhookParser._is_valid_json(spaced):
247
+ add(spaced)
248
+
249
+ collapsed = WebhookParser._collapse_triple_backslash_quotes(spaced)
250
+ if collapsed != request_body and WebhookParser._is_valid_json(collapsed):
251
+ add(collapsed)
252
+
253
+ if WebhookParser._is_valid_json(request_body):
254
+ add(request_body)
255
+
256
+ normalized = WebhookParser._normalize_over_escaped_quotes(request_body)
257
+ if normalized != request_body and WebhookParser._is_json_minified(normalized) and WebhookParser._is_valid_json(normalized):
258
+ add(normalized)
259
+
260
+ nested_processed = WebhookParser._process_nested_json_fields(request_body)
261
+ if nested_processed != request_body:
262
+ add(nested_processed)
263
+
264
+ minified, err = WebhookParser._ensure_minified_json(request_body)
265
+ if err:
266
+ if not forms:
267
+ raise ValueError("failed to prepare any signature body form") from err
268
+ else:
269
+ add(minified)
270
+
271
+ if not forms:
272
+ raise ValueError("failed to prepare any signature body form")
273
+ return forms
274
+
172
275
  @staticmethod
173
276
  def _sha256_lower_hex(data: str) -> str:
174
277
  return hashlib.sha256(data.encode("utf-8")).hexdigest()
@@ -179,13 +282,38 @@ class WebhookParser:
179
282
  relative_path_url: str,
180
283
  body: str,
181
284
  x_timestamp: str
182
- ) -> tuple[str, Exception]:
183
- processed_body, err = self._ensure_minified_json(body)
184
- if err:
185
- return "", Exception(f"_construct_string_to_verify: failed to ensure JSON is minified: {err}")
186
-
187
- body_hash = self._sha256_lower_hex(processed_body)
188
- return f"{http_method}:{relative_path_url}:{body_hash}:{x_timestamp}", None
285
+ ) -> str:
286
+ path = relative_path_url if relative_path_url.startswith("/") else f"/{relative_path_url}"
287
+ body_hash = self._sha256_lower_hex(body)
288
+ return f"{http_method.upper()}:{path}:{body_hash}:{x_timestamp}"
289
+
290
+ def _verify_signature(
291
+ self,
292
+ http_method: str,
293
+ relative_path_url: str,
294
+ body: str,
295
+ x_timestamp: str,
296
+ x_signature: str,
297
+ ) -> None:
298
+ body_forms = self._body_forms_for_signature(body)
299
+ signature_bytes = base64.b64decode(x_signature)
300
+
301
+ for body_form in body_forms:
302
+ string_to_verify = self._construct_string_to_verify(
303
+ http_method, relative_path_url, body_form, x_timestamp
304
+ )
305
+ try:
306
+ self.public_key.verify(
307
+ signature_bytes,
308
+ string_to_verify.encode("utf-8"),
309
+ padding.PKCS1v15(),
310
+ hashes.SHA256(),
311
+ )
312
+ return
313
+ except InvalidSignature:
314
+ continue
315
+
316
+ raise ValueError("Signature verification failed.")
189
317
 
190
318
  def parse_webhook(
191
319
  self,
@@ -200,33 +328,33 @@ class WebhookParser:
200
328
  if not x_signature or not x_timestamp:
201
329
  raise ValueError("Missing X-SIGNATURE or X-TIMESTAMP header.")
202
330
 
203
- string_to_verify, err = self._construct_string_to_verify(
204
- http_method=http_method,
205
- relative_path_url=relative_path_url,
206
- body=body,
207
- x_timestamp=x_timestamp
208
- )
209
- if err:
210
- raise ValueError(str(err))
211
- signature_bytes = base64.b64decode(x_signature)
212
- try:
213
- self.public_key.verify(
214
- signature_bytes,
215
- string_to_verify.encode('utf-8'),
216
- padding.PKCS1v15(),
217
- hashes.SHA256()
218
- )
219
- except InvalidSignature:
220
- raise ValueError("Signature verification failed.")
331
+ self._verify_signature(http_method, relative_path_url, body, x_timestamp, x_signature)
332
+
333
+ # Try multiple body transformations for parsing in order of likelihood:
334
+ # 1. Collapsed triple-backslash form (handles over-escaped \\\" → \")
335
+ # 2. Process nested JSON fields (handles bare-quote inner JSON like "field":"{...}")
336
+ # 3. ensureMinifiedJson fallback for pretty-printed bodies
337
+ payload_dict = None
338
+ for candidate in [
339
+ self._collapse_triple_backslash_quotes(body),
340
+ self._process_nested_json_fields(body),
341
+ ]:
342
+ try:
343
+ payload_dict = json.loads(candidate)
344
+ break
345
+ except json.JSONDecodeError:
346
+ continue
347
+
348
+ if payload_dict is None:
349
+ processed_body, err = self._ensure_minified_json(body)
350
+ if err:
351
+ raise ValueError(f"Failed to process JSON body: {err}")
352
+ try:
353
+ payload_dict = json.loads(processed_body)
354
+ except json.JSONDecodeError:
355
+ raise ValueError("Invalid JSON in request body.")
221
356
 
222
- processed_body, err = self._ensure_minified_json(body)
223
- if err:
224
- raise ValueError(f"Failed to process JSON body: {err}")
225
-
226
357
  try:
227
- payload_dict = json.loads(processed_body)
228
358
  return FinishNotifyRequest.from_dict(payload_dict)
229
- except json.JSONDecodeError:
230
- raise ValueError("Invalid JSON in request body.")
231
359
  except Exception as e:
232
360
  raise ValueError(f"Failed to parse body into FinishNotifyRequest: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dana-python
3
- Version: 2.1.8
3
+ Version: 2.1.9
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dana-python"
3
- version = "2.1.8"
3
+ version = "2.1.9"
4
4
  readme = "README.md"
5
5
  description = "API Client (SDK) for DANA APIs based on https://dashboard.dana.id/api-docs"
6
6
  license = "Apache-2.0"
@@ -0,0 +1,275 @@
1
+ # Copyright 2025 PT Espay Debit Indonesia Koe
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import os
17
+ from dana.webhook.finish_notify_request import FinishNotifyRequest
18
+ from dana.webhook import WebhookParser
19
+ from dana.utils.snap_header import SnapHeader
20
+ # Import fixtures directly from their modules to avoid circular imports
21
+ from tests.fixtures.api_client import api_instance_payment_gateway
22
+ from tests.fixtures.payment_gateway import webhook_key_pair
23
+
24
+ class TestWebhookParser:
25
+ def test_webhook_signature_and_parsing_success(self):
26
+ # Load keys from environment variables
27
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
28
+
29
+ webhook_http_method = "POST"
30
+ webhook_relative_url = "/api/v2/test-notif/dana"
31
+
32
+ webhook_body_str = '{"amount":{"currency":"IDR","value":"100000.00"},"originalReferenceNo":"20250916111230999500166191900293793","merchantId":"216620080007039826152","latestTransactionStatus":"00","additionalInfo":{"paidTime":"1757998714761","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"100000.00"},"payAmount":{"currency":"IDR","value":"100000.00"},"payMethod":"NETWORK_PAY","payOption":"NETWORK_PAY_PG_LINKAJA"}],"extendInfo":"{\"externalPromoInfos\":[]}"}},"originalPartnerReferenceNo":"LINKIT25091757998646","createdTime":"1757998647000","finishedTime":"1757998714761","transactionStatusDesc":"SUCCESS"}'
33
+
34
+ x_timestamp = "2025-09-16T12:11:30+07:00"
35
+ signature = "d/7mle7A+FCl4zvBZ2dMr3s7TVbbaK+toMtZwoev4OmLhn6Ctz/ynMaL3m3vHjAmV3UL3Fq5xp9thZFsO8BY74Vehqr1N9LQblV6i3TfMwT6lMvvhzWr0Fjbasyj23c5nFu1MOxpBiZFMMDNh8GQNLBAehHbjOmNldXSL6OQYRrK/TN9tdDyYFK6ltnKf4BN6bZa2ViAlI/np/U3QBW2LnDL82+8ZK7tYVF5bZyLLUSLXeWXBGQFTSDWqN+JdUHSnxGdQr3hZ7Y9Vqm/7G6rj6NrCxLPiEJYq1DQO3DjokMsORA2lOxzuo53bwqmhmD1mFhJKWF1JmyJuFdo2HB9JA=="
36
+
37
+ headers = {
38
+ "Channel-Id": "DANA",
39
+ "Charset": "UTF-8",
40
+ "Content-Type": "application/json",
41
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
42
+ "X-External-Id": "cFQ0PK5yS9wkMTMOVem4fIoUuyuA28jg",
43
+ "X-Partner-Id": "2025090110410957288340",
44
+ "X-Signature": signature,
45
+ "X-Timestamp": x_timestamp
46
+ }
47
+
48
+ # Create the parser with the public key
49
+ parser = WebhookParser(public_key=public_key)
50
+
51
+ # Verify and parse
52
+ result = parser.parse_webhook(
53
+ http_method=webhook_http_method,
54
+ relative_path_url=webhook_relative_url,
55
+ headers=headers,
56
+ body=webhook_body_str
57
+ )
58
+
59
+ # Verify specific fields like in the Go test
60
+ assert result is not None
61
+ assert result.original_partner_reference_no == "LINKIT25091757998646"
62
+ assert result.original_reference_no == "20250916111230999500166191900293793"
63
+ assert result.merchant_id == "216620080007039826152"
64
+ assert result.amount.value == "100000.00"
65
+ assert result.amount.currency == "IDR"
66
+ assert result.latest_transaction_status == "00"
67
+
68
+ def test_webhook_with_double_escaped_quotes(self):
69
+ """Test webhook parsing with double-escaped quotes in JSON (like PHP test)"""
70
+ # Load keys from environment variables
71
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
72
+
73
+ webhook_http_method = "POST"
74
+ webhook_relative_url = "/d34021fa-7599-413b-8743-ddab605fea49"
75
+
76
+ # This webhook body contains double-escaped quotes in extendInfo field
77
+ webhook_body_str = '{"amount":{"currency":"IDR","value":"50000.00"},"originalReferenceNo":"20251010111230999500166931000229476","merchantId":"216620090016041032029","latestTransactionStatus":"00","additionalInfo":{"paidTime":"2025-10-10T16:16:33+07:00","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"50000.00"},"payAmount":{"currency":"IDR","value":"50000.00"},"payMethod":"VIRTUAL_ACCOUNT","payOption":"VIRTUAL_ACCOUNT_BRI"}],"extendInfo":"{\\"externalPromoInfos\\":[]}"}},"originalPartnerReferenceNo":"ORDER-1760087736146","createdTime":"2025-10-10T16:15:37+07:00","finishedTime":"2025-10-10T16:16:33+07:00","transactionStatusDesc":"SUCCESS"}'
78
+
79
+ x_timestamp = "2025-10-13T13:43:30+07:00"
80
+ signature = "fqrQPxlzEN4ZGW9vYt3PokmIrbG2HQtlbdj6krjf9HFW1qS3ilZjSR+9Z4XZNYxQIxyHHqXmjEiBU4ui/JrknSXlCpPQe7DztB/Ye+yLxIHYBnwdeCXn2zGGAV51nQki+eD2aL8Z6d6MyWz9hoytwE+jtWKUC0KtU7wQfoB0XjdEXzU3/4Ao/rWQbt97UONaaf7i5l3+M/ICP187PYw9iRHLUFh7WRPs8JKpZyO0kcJqEJbeOUjHMmIsLQImOlYVbQTM/1v89Ou1WcVAo0cXNE5yrvosB4pQROeI8KY2X1FNTuB1pdFtQTIyUcd/t1wuIxqHqqFKjrFdcQxlZOIyRg=="
81
+
82
+ headers = {
83
+ "Channel-Id": "DANA",
84
+ "Charset": "UTF-8",
85
+ "Content-Type": "application/json",
86
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
87
+ "X-External-Id": "nXPTWcwt7lgCOy01wWGKDerEMlKwV5wc",
88
+ "X-Partner-Id": "2025091611385324660336",
89
+ "X-Signature": signature,
90
+ "X-Timestamp": x_timestamp
91
+ }
92
+
93
+ # Create the parser with the public key
94
+ parser = WebhookParser(public_key=public_key)
95
+
96
+ # Verify and parse - this should work with the JSON normalization
97
+ result = parser.parse_webhook(
98
+ http_method=webhook_http_method,
99
+ relative_path_url=webhook_relative_url,
100
+ headers=headers,
101
+ body=webhook_body_str
102
+ )
103
+
104
+ # Verify specific fields like in the PHP test
105
+ assert result is not None
106
+ assert result.original_partner_reference_no == "ORDER-1760087736146"
107
+ assert result.original_reference_no == "20251010111230999500166931000229476"
108
+ assert result.merchant_id == "216620090016041032029"
109
+ assert result.amount.value == "50000.00"
110
+ assert result.amount.currency == "IDR"
111
+ assert result.latest_transaction_status == "00"
112
+
113
+ def test_over_escaped_extend_info(self):
114
+ """extendInfo contains triple-backslash-escaped quotes (\\\\\\\" instead of \\\") as
115
+ received through a proxy/gateway log that over-escapes nested JSON."""
116
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
117
+
118
+ webhook_http_method = "POST"
119
+ webhook_relative_url = "/d34021fa-7599-413b-8743-ddab605fea49"
120
+
121
+ webhook_body_str = '{"amount":{"currency":"IDR","value":"50000.00"},"originalReferenceNo":"20251010111230999500166931000229476","merchantId":"216620090016041032029","latestTransactionStatus":"00","additionalInfo":{"paidTime":"2025-10-10T16:16:33+07:00","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"50000.00"},"payAmount":{"currency":"IDR","value":"50000.00"},"payMethod":"VIRTUAL_ACCOUNT","payOption":"VIRTUAL_ACCOUNT_BRI"}],"extendInfo":"{\\\\\\"externalPromoInfos\\\\\\":[]}"}},"originalPartnerReferenceNo":"ORDER-1760087736146","createdTime":"2025-10-10T16:15:37+07:00","finishedTime":"2025-10-10T16:16:33+07:00","transactionStatusDesc":"SUCCESS"}'
122
+
123
+ x_timestamp = "2025-10-13T13:43:30+07:00"
124
+ signature = "fqrQPxlzEN4ZGW9vYt3PokmIrbG2HQtlbdj6krjf9HFW1qS3ilZjSR+9Z4XZNYxQIxyHHqXmjEiBU4ui/JrknSXlCpPQe7DztB/Ye+yLxIHYBnwdeCXn2zGGAV51nQki+eD2aL8Z6d6MyWz9hoytwE+jtWKUC0KtU7wQfoB0XjdEXzU3/4Ao/rWQbt97UONaaf7i5l3+M/ICP187PYw9iRHLUFh7WRPs8JKpZyO0kcJqEJbeOUjHMmIsLQImOlYVbQTM/1v89Ou1WcVAo0cXNE5yrvosB4pQROeI8KY2X1FNTuB1pdFtQTIyUcd/t1wuIxqHqqFKjrFdcQxlZOIyRg=="
125
+
126
+ headers = {
127
+ "Channel-Id": "DANA",
128
+ "Charset": "UTF-8",
129
+ "Content-Type": "application/json",
130
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
131
+ "X-External-Id": "dA8h1wYA5doFvTUZaIVabp3EQBXTfb01",
132
+ "X-Partner-Id": "2025091611385324660336",
133
+ "X-Signature": signature,
134
+ "X-Timestamp": x_timestamp,
135
+ }
136
+
137
+ parser = WebhookParser(public_key=public_key)
138
+ result = parser.parse_webhook(
139
+ http_method=webhook_http_method,
140
+ relative_path_url=webhook_relative_url,
141
+ headers=headers,
142
+ body=webhook_body_str,
143
+ )
144
+
145
+ assert result is not None
146
+ assert result.original_partner_reference_no == "ORDER-1760087736146"
147
+ assert result.original_reference_no == "20251010111230999500166931000229476"
148
+ assert result.merchant_id == "216620090016041032029"
149
+ assert result.amount.value == "50000.00"
150
+ assert result.amount.currency == "IDR"
151
+ assert result.latest_transaction_status == "00"
152
+ assert result.transaction_status_desc == "SUCCESS"
153
+
154
+ def test_production_qris_wire_bytes(self):
155
+ """Exact wire bytes as sent by the DANA gateway (no extra escaping).
156
+ Python raw string r'...' is equivalent to JS String.raw."""
157
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
158
+
159
+ webhook_http_method = "POST"
160
+ webhook_relative_url = "/v2/dana/callback-qris"
161
+
162
+ webhook_body_str = r'{"amount":{"currency":"IDR","value":"1000.00"},"originalReferenceNo":"20260521111212800100166771803184279","merchantId":"216620040007047069653","latestTransactionStatus":"00","additionalInfo":{"shopInfo":{"externalShopId":"a9ab9ac5","shopName":"PT REKA MIKRO MOBILITAS","shopId":"216660000003346861448","shopAddress":"{\"address1\":\"a9ab9ac5\",\"address2\":\"a9ab9ac5\",\"area\":\"Abiansemal\",\"city\":\"Kab. Badung\",\"contactAddressId\":\"120100000003577990444\",\"contactAddressType\":\"OFFICE_ADD\",\"country\":\"Indonesia\",\"defaultAddress\":false,\"province\":\"Bali\",\"verified\":true,\"zipcode\":\"80351\"}"},"tipsAmount":{"amount":"0.0","centFactor":"100","cent":"0","currencyValue":"360","currency":"IDR","currencyCode":"IDR"},"extendInfo":"{\"payment_scene\":\"C_SCAN_B\",\"QR_TYPE\":\"QR_DYNAMIC\",\"externalShopId\":\"a9ab9ac5\",\"osType\":\"android\",\"sourcePlatform\":\"MAIN_APP\",\"billNumber\":\"TRX20260521847fe0\"}","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"1000.00"},"payAmount":{"currency":"IDR","value":"1000.00"},"payMethod":"BALANCE","chargeAmount":{"currency":"IDR","value":"0.00"},"extendInfo":"{}","payOptionBillExtendInfo":"{}"}],"cashierRequestId":"4c0aaad0748e393d528fab8fc7b76599","paidTime":"2026-05-21T11:35:32+07:00","payRequestExtendInfo":"{\"payment_scene\":\"C_SCAN_B\",\"supportNewCashierFlow\":\"false\",\"EMVCO_CODE_INFO\":\"{\\\"acquiringBankName\\\":\\\"DANA\\\",\\\"additionalInfo\\\":{\\\"billNumber\\\":\\\"TRX20260521847fe0\\\",\\\"terminalLabel\\\":\\\"MER2026042717424830271473\\\"},\\\"countryCode\\\":\\\"ID\\\",\\\"creditAccountInfos\\\":[],\\\"extendInfo\\\":{},\\\"externalSerialNo\\\":\\\"771803184279\\\",\\\"gpnMerchantId\\\":\\\"216660000003346861448-a9ab9ac5\\\",\\\"instId\\\":\\\"DANA\\\",\\\"merchantCity\\\":\\\"Kab. Badung\\\",\\\"merchantNameLocation\\\":\\\"PT REKA MIKRO MOBILITAS\\\",\\\"merchantPan\\\":\\\"936009150002729888\\\",\\\"merchantPanLuhn\\\":\\\"9360091500027298882\\\",\\\"merchantType\\\":\\\"PSO\\\",\\\"onUs\\\":true,\\\"postalCode\\\":\\\"80351\\\",\\\"qrInfoCacheIndex\\\":\\\"MO_EMVCO_PARSE_CACHEGZ009B9B87A57BFF4170819754F3C0033863danabizpluginGZ001779338127709\\\",\\\"trxCode\\\":\\\"Payment Credit\\\",\\\"trxFeeAmount\\\":{\\\"amount\\\":0.00,\\\"cent\\\":0,\\\"centFactor\\\":100,\\\"currency\\\":\\\"IDR\\\",\\\"currencyCode\\\":\\\"IDR\\\",\\\"currencyValue\\\":\\\"360\\\"}}\",\"isClientSupportFaceAuth\":\"false\",\"callbackClientVersion\":\"2.1\",\"passThroughToPromotion\":\"{\\\"ORDER_TITLE\\\":\\\"Pay to PT REKA MIKRO MOBILITAS\\\",\\\"gpnMerchantId\\\":\\\"216660000003346861448-a9ab9ac5\\\",\\\"CLIENT_ID\\\":\\\"2026042717424830271473\\\",\\\"SHOP_INFO\\\":\\\"{\\\\\\\"externalShopId\\\\\\\":\\\\\\\"a9ab9ac5\\\\\\\",\\\\\\\"mccCodes\\\\\\\":[\\\\\\\"4789\\\\\\\"],\\\\\\\"shopAddress\\\\\\\":\\\\\\\"{\\\\\\\\\\\\\\\"address1\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"address2\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"area\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Abiansemal\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"city\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Kab. Badung\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"contactAddressId\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"120100000003577990444\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"contactAddressType\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"OFFICE_ADD\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"country\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Indonesia\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"defaultAddress\\\\\\\\\\\\\\\":false,\\\\\\\\\\\\\\\"province\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Bali\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"verified\\\\\\\\\\\\\\\":true,\\\\\\\\\\\\\\\"zipcode\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"80351\\\\\\\\\\\\\\\"}\\\\\\\",\\\\\\\"shopId\\\\\\\":\\\\\\\"216660000003346861448\\\\\\\",\\\\\\\"shopName\\\\\\\":\\\\\\\"PT REKA MIKRO MOBILITAS\\\\\\\"}\\\"}\",\"orderStatus\":\"INIT\",\"SERVICE_INFO\":\"null\",\"isFrictionless\":\"false\",\"passToFluxnet\":\"{\\\"onUs\\\":\\\"true\\\",\\\"merchantName\\\":\\\"PT REKA MIKRO MOBILITAS\\\"}\",\"CHARGE_USER_FEE_INFO\":\"[]\",\"merchantCategoryCode\":\"4789\",\"externalShopId\":\"a9ab9ac5\",\"merchantCategoryName\":\"TRANSPORTATION SERVICES (NOT ELSEWHERE CLASSIFIED)\",\"needAmlCheck\":\"false\",\"billNumber\":\"TRX20260521847fe0\",\"passThroughToRisk\":\"{\\\"isModifySmartpay\\\":\\\"false\\\",\\\"passkeys\\\":\\\"false\\\",\\\"isPasskeysSupported\\\":\\\"false\\\",\\\"isSupportWAOtp\\\":\\\"true\\\",\\\"isFrictionless\\\":\\\"false\\\",\\\"payerTypingChallenge\\\":\\\"false\\\"}\",\"SUPPORT_FRICTIONLESS\":\"false\"}","extendInfo":"{\"topupAndPay\":\"false\",\"paymentStatus\":\"SUCCESS\"}"}},"externalStoreID":"a9ab9ac5","originalPartnerReferenceNo":"TRX20260521847fe0","finishedTime":"2026-05-21T11:35:32+07:00","createdTime":"2026-05-21T11:35:27+07:00","transactionStatusDesc":"SUCCESS"}'
163
+
164
+ x_timestamp = "2026-05-25T15:03:30+07:00"
165
+ signature = "dF/ljaqWsl4j93/z0yXGzbh/LBCg+XVi9bDshz4pKbdbRVP923Gb0mHx0ouMpbV0MWLOZdRlumSs9zMdmdsgCN7ED1kRoZV2f61TXb14aEMtWwEW7sLFSSMOTFq1nCn1lzYEKvuzPgMuypBg2CJzECrRenIjC2R3Paj6NfbM1PfQAA5Gqz1vTNsYlX7P5DAxZasG5miTY7WqCACl+o9MAwHxHf9RNiE2vVn9uy9mc1PTMWByEW9eVYY8PX6/sjceQz2HeXNmYuxkA1lP1y5UUwmdxxiWdyGJeJkSL+HYpaNRwAEO+7WgTmvTX1oM1MxNkFm2mbgYQoyW8K0rXZmcKQ=="
166
+
167
+ headers = {
168
+ "Channel-Id": "DANA",
169
+ "Charset": "UTF-8",
170
+ "Content-Type": "application/json",
171
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
172
+ "X-External-Id": "yA07YWHKqVsCFEk8ivCzAz16nmtj51iF",
173
+ "X-Partner-Id": "2026042717424830271473",
174
+ "X-Signature": signature,
175
+ "X-Timestamp": x_timestamp,
176
+ }
177
+
178
+ parser = WebhookParser(public_key=public_key)
179
+ result = parser.parse_webhook(
180
+ http_method=webhook_http_method,
181
+ relative_path_url=webhook_relative_url,
182
+ headers=headers,
183
+ body=webhook_body_str,
184
+ )
185
+
186
+ assert result is not None
187
+ assert result.original_partner_reference_no == "TRX20260521847fe0"
188
+ assert result.original_reference_no == "20260521111212800100166771803184279"
189
+ assert result.merchant_id == "216620040007047069653"
190
+ assert result.amount.value == "1000.00"
191
+ assert result.amount.currency == "IDR"
192
+ assert result.latest_transaction_status == "00"
193
+ assert result.transaction_status_desc == "SUCCESS"
194
+
195
+ def test_production_qris_js_escaped_body(self):
196
+ """Same production QRIS payload but received as a JS-style escaped string.
197
+ Python and JS single-quoted strings use the same escape rules for \\\\ sequences."""
198
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
199
+
200
+ webhook_http_method = "POST"
201
+ webhook_relative_url = "/v2/dana/callback-qris"
202
+
203
+ webhook_body_str = '{"amount":{"currency":"IDR","value":"1000.00"},"originalReferenceNo":"20260521111212800100166771803184279","merchantId":"216620040007047069653","latestTransactionStatus":"00","additionalInfo":{"shopInfo":{"externalShopId":"a9ab9ac5","shopName":"PT REKA MIKRO MOBILITAS","shopId":"216660000003346861448","shopAddress":"{\\"address1\\":\\"a9ab9ac5\\",\\"address2\\":\\"a9ab9ac5\\",\\"area\\":\\"Abiansemal\\",\\"city\\":\\"Kab. Badung\\",\\"contactAddressId\\":\\"120100000003577990444\\",\\"contactAddressType\\":\\"OFFICE_ADD\\",\\"country\\":\\"Indonesia\\",\\"defaultAddress\\":false,\\"province\\":\\"Bali\\",\\"verified\\":true,\\"zipcode\\":\\"80351\\"}"},"tipsAmount":{"amount":"0.0","centFactor":"100","cent":"0","currencyValue":"360","currency":"IDR","currencyCode":"IDR"},"extendInfo":"{\\"payment_scene\\":\\"C_SCAN_B\\",\\"QR_TYPE\\":\\"QR_DYNAMIC\\",\\"externalShopId\\":\\"a9ab9ac5\\",\\"osType\\":\\"android\\",\\"sourcePlatform\\":\\"MAIN_APP\\",\\"billNumber\\":\\"TRX20260521847fe0\\"}","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"1000.00"},"payAmount":{"currency":"IDR","value":"1000.00"},"payMethod":"BALANCE","chargeAmount":{"currency":"IDR","value":"0.00"},"extendInfo":"{}","payOptionBillExtendInfo":"{}"}],"cashierRequestId":"4c0aaad0748e393d528fab8fc7b76599","paidTime":"2026-05-21T11:35:32+07:00","payRequestExtendInfo":"{\\"payment_scene\\":\\"C_SCAN_B\\",\\"supportNewCashierFlow\\":\\"false\\",\\"EMVCO_CODE_INFO\\":\\"{\\\\\\"acquiringBankName\\\\\\":\\\\\\"DANA\\\\\\",\\\\\\"additionalInfo\\\\\\":{\\\\\\"billNumber\\\\\\":\\\\\\"TRX20260521847fe0\\\\\\",\\\\\\"terminalLabel\\\\\\":\\\\\\"MER2026042717424830271473\\\\\\"},\\\\\\"countryCode\\\\\\":\\\\\\"ID\\\\\\",\\\\\\"creditAccountInfos\\\\\\":[],\\\\\\"extendInfo\\\\\\":{},\\\\\\"externalSerialNo\\\\\\":\\\\\\"771803184279\\\\\\",\\\\\\"gpnMerchantId\\\\\\":\\\\\\"216660000003346861448-a9ab9ac5\\\\\\",\\\\\\"instId\\\\\\":\\\\\\"DANA\\\\\\",\\\\\\"merchantCity\\\\\\":\\\\\\"Kab. Badung\\\\\\",\\\\\\"merchantNameLocation\\\\\\":\\\\\\"PT REKA MIKRO MOBILITAS\\\\\\",\\\\\\"merchantPan\\\\\\":\\\\\\"936009150002729888\\\\\\",\\\\\\"merchantPanLuhn\\\\\\":\\\\\\"9360091500027298882\\\\\\",\\\\\\"merchantType\\\\\\":\\\\\\"PSO\\\\\\",\\\\\\"onUs\\\\\\":true,\\\\\\"postalCode\\\\\\":\\\\\\"80351\\\\\\",\\\\\\"qrInfoCacheIndex\\\\\\":\\\\\\"MO_EMVCO_PARSE_CACHEGZ009B9B87A57BFF4170819754F3C0033863danabizpluginGZ001779338127709\\\\\\",\\\\\\"trxCode\\\\\\":\\\\\\"Payment Credit\\\\\\",\\\\\\"trxFeeAmount\\\\\\":{\\\\\\"amount\\\\\\":0.00,\\\\\\"cent\\\\\\":0,\\\\\\"centFactor\\\\\\":100,\\\\\\"currency\\\\\\":\\\\\\"IDR\\\\\\",\\\\\\"currencyCode\\\\\\":\\\\\\"IDR\\\\\\",\\\\\\"currencyValue\\\\\\":\\\\\\"360\\\\\\"}}\\",\\"isClientSupportFaceAuth\\":\\"false\\",\\"callbackClientVersion\\":\\"2.1\\",\\"passThroughToPromotion\\":\\"{\\\\\\"ORDER_TITLE\\\\\\":\\\\\\"Pay to PT REKA MIKRO MOBILITAS\\\\\\",\\\\\\"gpnMerchantId\\\\\\":\\\\\\"216660000003346861448-a9ab9ac5\\\\\\",\\\\\\"CLIENT_ID\\\\\\":\\\\\\"2026042717424830271473\\\\\\",\\\\\\"SHOP_INFO\\\\\\":\\\\\\"{\\\\\\\\\\\\\\"externalShopId\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"mccCodes\\\\\\\\\\\\\\":[\\\\\\\\\\\\\\"4789\\\\\\\\\\\\\\"],\\\\\\\\\\\\\\"shopAddress\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"{\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"address1\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"address2\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"area\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Abiansemal\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"city\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Kab. Badung\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"contactAddressId\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"120100000003577990444\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"contactAddressType\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"OFFICE_ADD\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"country\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Indonesia\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"defaultAddress\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":false,\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"province\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Bali\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"verified\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":true,\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"zipcode\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"80351\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"}\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"shopId\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"216660000003346861448\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"shopName\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"PT REKA MIKRO MOBILITAS\\\\\\\\\\\\\\"}\\\\\\"}\\",\\"orderStatus\\":\\"INIT\\",\\"SERVICE_INFO\\":\\"null\\",\\"isFrictionless\\":\\"false\\",\\"passToFluxnet\\":\\"{\\\\\\"onUs\\\\\\":\\\\\\"true\\\\\\",\\\\\\"merchantName\\\\\\":\\\\\\"PT REKA MIKRO MOBILITAS\\\\\\"}\\",\\"CHARGE_USER_FEE_INFO\\":\\"[]\\",\\"merchantCategoryCode\\":\\"4789\\",\\"externalShopId\\":\\"a9ab9ac5\\",\\"merchantCategoryName\\":\\"TRANSPORTATION SERVICES (NOT ELSEWHERE CLASSIFIED)\\",\\"needAmlCheck\\":\\"false\\",\\"billNumber\\":\\"TRX20260521847fe0\\",\\"passThroughToRisk\\":\\"{\\\\\\"isModifySmartpay\\\\\\":\\\\\\"false\\\\\\",\\\\\\"passkeys\\\\\\":\\\\\\"false\\\\\\",\\\\\\"isPasskeysSupported\\\\\\":\\\\\\"false\\\\\\",\\\\\\"isSupportWAOtp\\\\\\":\\\\\\"true\\\\\\",\\\\\\"isFrictionless\\\\\\":\\\\\\"false\\\\\\",\\\\\\"payerTypingChallenge\\\\\\":\\\\\\"false\\\\\\"}\\",\\"SUPPORT_FRICTIONLESS\\":\\"false\\"}","extendInfo":"{\\"topupAndPay\\":\\"false\\",\\"paymentStatus\\":\\"SUCCESS\\"}"}},"externalStoreID":"a9ab9ac5","originalPartnerReferenceNo":"TRX20260521847fe0","finishedTime":"2026-05-21T11:35:32+07:00","createdTime":"2026-05-21T11:35:27+07:00","transactionStatusDesc":"SUCCESS"}'
204
+
205
+ x_timestamp = "2026-05-25T15:03:30+07:00"
206
+ signature = "dF/ljaqWsl4j93/z0yXGzbh/LBCg+XVi9bDshz4pKbdbRVP923Gb0mHx0ouMpbV0MWLOZdRlumSs9zMdmdsgCN7ED1kRoZV2f61TXb14aEMtWwEW7sLFSSMOTFq1nCn1lzYEKvuzPgMuypBg2CJzECrRenIjC2R3Paj6NfbM1PfQAA5Gqz1vTNsYlX7P5DAxZasG5miTY7WqCACl+o9MAwHxHf9RNiE2vVn9uy9mc1PTMWByEW9eVYY8PX6/sjceQz2HeXNmYuxkA1lP1y5UUwmdxxiWdyGJeJkSL+HYpaNRwAEO+7WgTmvTX1oM1MxNkFm2mbgYQoyW8K0rXZmcKQ=="
207
+
208
+ headers = {
209
+ "Channel-Id": "DANA",
210
+ "Charset": "UTF-8",
211
+ "Content-Type": "application/json",
212
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
213
+ "X-External-Id": "yA07YWHKqVsCFEk8ivCzAz16nmtj51iF",
214
+ "X-Partner-Id": "2026042717424830271473",
215
+ "X-Signature": signature,
216
+ "X-Timestamp": x_timestamp,
217
+ }
218
+
219
+ parser = WebhookParser(public_key=public_key)
220
+ result = parser.parse_webhook(
221
+ http_method=webhook_http_method,
222
+ relative_path_url=webhook_relative_url,
223
+ headers=headers,
224
+ body=webhook_body_str,
225
+ )
226
+
227
+ assert result is not None
228
+ assert result.original_partner_reference_no == "TRX20260521847fe0"
229
+ assert result.original_reference_no == "20260521111212800100166771803184279"
230
+ assert result.merchant_id == "216620040007047069653"
231
+ assert result.amount.value == "1000.00"
232
+ assert result.amount.currency == "IDR"
233
+ assert result.latest_transaction_status == "00"
234
+ assert result.transaction_status_desc == "SUCCESS"
235
+
236
+ def test_production_qris_finish_notify_wire_bytes(self):
237
+ """QRIS finish-notify webhook — exact wire bytes (raw string, no extra escaping).
238
+ Tests that the FinishNotify-style QRIS payload is correctly verified and parsed."""
239
+ public_key = os.getenv("WEBHOOK_PUBLIC_KEY")
240
+
241
+ webhook_http_method = "POST"
242
+ webhook_relative_url = "/v2/dana/callback-qris"
243
+
244
+ webhook_body_str = r'{"amount":{"currency":"IDR","value":"1000.00"},"originalReferenceNo":"20260521111212800100166771803184279","merchantId":"216620040007047069653","latestTransactionStatus":"00","additionalInfo":{"shopInfo":{"externalShopId":"a9ab9ac5","shopName":"PT REKA MIKRO MOBILITAS","shopId":"216660000003346861448","shopAddress":"{\"address1\":\"a9ab9ac5\",\"address2\":\"a9ab9ac5\",\"area\":\"Abiansemal\",\"city\":\"Kab. Badung\",\"contactAddressId\":\"120100000003577990444\",\"contactAddressType\":\"OFFICE_ADD\",\"country\":\"Indonesia\",\"defaultAddress\":false,\"province\":\"Bali\",\"verified\":true,\"zipcode\":\"80351\"}"},"tipsAmount":{"amount":"0.0","centFactor":"100","cent":"0","currencyValue":"360","currency":"IDR","currencyCode":"IDR"},"extendInfo":"{\"payment_scene\":\"C_SCAN_B\",\"QR_TYPE\":\"QR_DYNAMIC\",\"externalShopId\":\"a9ab9ac5\",\"osType\":\"android\",\"sourcePlatform\":\"MAIN_APP\",\"billNumber\":\"TRX20260521847fe0\"}","paymentInfo":{"payOptionInfos":[{"transAmount":{"currency":"IDR","value":"1000.00"},"payAmount":{"currency":"IDR","value":"1000.00"},"payMethod":"BALANCE","chargeAmount":{"currency":"IDR","value":"0.00"},"extendInfo":"{}","payOptionBillExtendInfo":"{}"}],"cashierRequestId":"4c0aaad0748e393d528fab8fc7b76599","paidTime":"2026-05-21T11:35:32+07:00","payRequestExtendInfo":"{\"payment_scene\":\"C_SCAN_B\",\"supportNewCashierFlow\":\"false\",\"EMVCO_CODE_INFO\":\"{\\\"acquiringBankName\\\":\\\"DANA\\\",\\\"additionalInfo\\\":{\\\"billNumber\\\":\\\"TRX20260521847fe0\\\",\\\"terminalLabel\\\":\\\"MER2026042717424830271473\\\"},\\\"countryCode\\\":\\\"ID\\\",\\\"creditAccountInfos\\\":[],\\\"extendInfo\\\":{},\\\"externalSerialNo\\\":\\\"771803184279\\\",\\\"gpnMerchantId\\\":\\\"216660000003346861448-a9ab9ac5\\\",\\\"instId\\\":\\\"DANA\\\",\\\"merchantCity\\\":\\\"Kab. Badung\\\",\\\"merchantNameLocation\\\":\\\"PT REKA MIKRO MOBILITAS\\\",\\\"merchantPan\\\":\\\"936009150002729888\\\",\\\"merchantPanLuhn\\\":\\\"9360091500027298882\\\",\\\"merchantType\\\":\\\"PSO\\\",\\\"onUs\\\":true,\\\"postalCode\\\":\\\"80351\\\",\\\"qrInfoCacheIndex\\\":\\\"MO_EMVCO_PARSE_CACHEGZ009B9B87A57BFF4170819754F3C0033863danabizpluginGZ001779338127709\\\",\\\"trxCode\\\":\\\"Payment Credit\\\",\\\"trxFeeAmount\\\":{\\\"amount\\\":0.00,\\\"cent\\\":0,\\\"centFactor\\\":100,\\\"currency\\\":\\\"IDR\\\",\\\"currencyCode\\\":\\\"IDR\\\",\\\"currencyValue\\\":\\\"360\\\"}}\",\"isClientSupportFaceAuth\":\"false\",\"callbackClientVersion\":\"2.1\",\"passThroughToPromotion\":\"{\\\"ORDER_TITLE\\\":\\\"Pay to PT REKA MIKRO MOBILITAS\\\",\\\"gpnMerchantId\\\":\\\"216660000003346861448-a9ab9ac5\\\",\\\"CLIENT_ID\\\":\\\"2026042717424830271473\\\",\\\"SHOP_INFO\\\":\\\"{\\\\\\\"externalShopId\\\\\\\":\\\\\\\"a9ab9ac5\\\\\\\",\\\\\\\"mccCodes\\\\\\\":[\\\\\\\"4789\\\\\\\"],\\\\\\\"shopAddress\\\\\\\":\\\\\\\"{\\\\\\\\\\\\\\\"address1\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"address2\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"a9ab9ac5\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"area\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Abiansemal\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"city\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Kab. Badung\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"contactAddressId\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"120100000003577990444\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"contactAddressType\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"OFFICE_ADD\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"country\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Indonesia\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"defaultAddress\\\\\\\\\\\\\\\":false,\\\\\\\\\\\\\\\"province\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"Bali\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"verified\\\\\\\\\\\\\\\":true,\\\\\\\\\\\\\\\"zipcode\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"80351\\\\\\\\\\\\\\\"}\\\\\\\",\\\\\\\"shopId\\\\\\\":\\\\\\\"216660000003346861448\\\\\\\",\\\\\\\"shopName\\\\\\\":\\\\\\\"PT REKA MIKRO MOBILITAS\\\\\\\"}\\\"}\",\"orderStatus\":\"INIT\",\"SERVICE_INFO\":\"null\",\"isFrictionless\":\"false\",\"passToFluxnet\":\"{\\\"onUs\\\":\\\"true\\\",\\\"merchantName\\\":\\\"PT REKA MIKRO MOBILITAS\\\"}\",\"CHARGE_USER_FEE_INFO\":\"[]\",\"merchantCategoryCode\":\"4789\",\"externalShopId\":\"a9ab9ac5\",\"merchantCategoryName\":\"TRANSPORTATION SERVICES (NOT ELSEWHERE CLASSIFIED)\",\"needAmlCheck\":\"false\",\"billNumber\":\"TRX20260521847fe0\",\"passThroughToRisk\":\"{\\\"isModifySmartpay\\\":\\\"false\\\",\\\"passkeys\\\":\\\"false\\\",\\\"isPasskeysSupported\\\":\\\"false\\\",\\\"isSupportWAOtp\\\":\\\"true\\\",\\\"isFrictionless\\\":\\\"false\\\",\\\"payerTypingChallenge\\\":\\\"false\\\"}\",\"SUPPORT_FRICTIONLESS\":\"false\"}","extendInfo":"{\"topupAndPay\":\"false\",\"paymentStatus\":\"SUCCESS\"}"}},"externalStoreID":"a9ab9ac5","originalPartnerReferenceNo":"TRX20260521847fe0","finishedTime":"2026-05-21T11:35:32+07:00","createdTime":"2026-05-21T11:35:27+07:00","transactionStatusDesc":"SUCCESS"}'
245
+
246
+ x_timestamp = "2026-05-25T15:03:30+07:00"
247
+ signature = "dF/ljaqWsl4j93/z0yXGzbh/LBCg+XVi9bDshz4pKbdbRVP923Gb0mHx0ouMpbV0MWLOZdRlumSs9zMdmdsgCN7ED1kRoZV2f61TXb14aEMtWwEW7sLFSSMOTFq1nCn1lzYEKvuzPgMuypBg2CJzECrRenIjC2R3Paj6NfbM1PfQAA5Gqz1vTNsYlX7P5DAxZasG5miTY7WqCACl+o9MAwHxHf9RNiE2vVn9uy9mc1PTMWByEW9eVYY8PX6/sjceQz2HeXNmYuxkA1lP1y5UUwmdxxiWdyGJeJkSL+HYpaNRwAEO+7WgTmvTX1oM1MxNkFm2mbgYQoyW8K0rXZmcKQ=="
248
+
249
+ headers = {
250
+ "Channel-Id": "DANA",
251
+ "Charset": "UTF-8",
252
+ "Content-Type": "application/json",
253
+ "User-Agent": "Jakarta Commons-HttpClient/3.1",
254
+ "X-External-Id": "yA07YWHKqVsCFEk8ivCzAz16nmtj51iF",
255
+ "X-Partner-Id": "2026042717424830271473",
256
+ "X-Signature": signature,
257
+ "X-Timestamp": x_timestamp,
258
+ }
259
+
260
+ parser = WebhookParser(public_key=public_key)
261
+ result = parser.parse_webhook(
262
+ http_method=webhook_http_method,
263
+ relative_path_url=webhook_relative_url,
264
+ headers=headers,
265
+ body=webhook_body_str,
266
+ )
267
+
268
+ assert result is not None
269
+ assert result.original_partner_reference_no == "TRX20260521847fe0"
270
+ assert result.original_reference_no == "20260521111212800100166771803184279"
271
+ assert result.merchant_id == "216620040007047069653"
272
+ assert result.amount.value == "1000.00"
273
+ assert result.amount.currency == "IDR"
274
+ assert result.latest_transaction_status == "00"
275
+ assert result.transaction_status_desc == "SUCCESS"