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.
- {dana_python-2.1.8/dana_python.egg-info → dana_python-2.1.9}/PKG-INFO +1 -1
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/shop_info.py +1 -1
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/webhook.py +173 -45
- {dana_python-2.1.8 → dana_python-2.1.9/dana_python.egg-info}/PKG-INFO +1 -1
- {dana_python-2.1.8 → dana_python-2.1.9}/pyproject.toml +1 -1
- dana_python-2.1.9/tests/test_webhook.py +275 -0
- dana_python-2.1.8/tests/test_webhook.py +0 -111
- {dana_python-2.1.8 → dana_python-2.1.9}/LICENSE +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/README.md +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/api_client.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/api_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/configuration.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/model.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/base/types.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/api/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/api/disbursement_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/enum.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/bank_account_inquiry_response_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/dana_account_inquiry_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/money.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_inquiry_status_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_inquiry_status_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_bank_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_inquiry_status_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_inquiry_status_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/disbursement/v1/models/transfer_to_dana_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/exceptions.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/api/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/api/merchant_management_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/enum.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/address_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/asset_card_list_item.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/business_docs.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_request_ext_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_division_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/create_shop_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/division_resource_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/member_asset_result_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_account_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_certificate_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_address.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_email.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_contact_mobile_no.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_corporate_certificate.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_information.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/merchant_resource_information.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/mobile_no_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/pic_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_asset_card_list_response_response.py +0 -0
- {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
- {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
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_division_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_info_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_merchant_resource_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/query_shop_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/result_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/shop_resource_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_division_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/update_shop_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/merchant_management/v1/models/user_name.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/api/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/api/payment_gateway_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/custom_validation.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/enum.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/actor_context.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/amount_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/audit_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/buyer.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/cancel_order_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/cancel_order_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_payment_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/consult_pay_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_api_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_api_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_redirect_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_by_redirect_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/create_order_response_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/env_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/goods.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/money.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/order_api_object.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/order_redirect_object.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/pay_option_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/payment_view.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/promo_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/query_payment_response_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_option_bill.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/refund_order_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/seller.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/shipping_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/status_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/time_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/url_param.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/payment_gateway/v1/models/virtual_account_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/rest.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/date_validation.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/models.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/open_api_configuration.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/open_api_header.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/script.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/snap_configuration.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/snap_header.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/utils/url.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_payment_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/finish_notify_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/money.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/webhook/pay_option_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/api/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/api/widget_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/custom_validation.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/enum.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/__init__.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/account_unbinding_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/amount_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_ott_response_user_resources_inner.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_authorization_code_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_refresh_token_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/apply_token_response_additional_info_user_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/balance_inquiry_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/buyer.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/cancel_order_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/cancel_order_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/env_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/goods.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/international_order_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/international_order_info_exchange_rate.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/money.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/oauth2_url_data.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/oauth2_url_data_seamless_data.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/order.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_detail_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/pay_option_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/payment_promo_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/payment_view.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_payment_response_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response_body.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/query_user_profile_response_response_head.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_order_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/refund_promo_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/result_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/seller.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/service_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/shipping_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/status_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/time_detail.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/url_param.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/user_resource_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/virtual_account_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_request.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_request_additional_info.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/models/widget_payment_response.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana/widget/v1/util.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/SOURCES.txt +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/dependency_links.txt +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/requires.txt +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/dana_python.egg-info/top_level.txt +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/setup.cfg +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_disbursement_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_merchant_management_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_payment_gateway_api.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_payment_gateway_with_automation.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_snap_header.py +0 -0
- {dana_python-2.1.8 → dana_python-2.1.9}/tests/test_widget_api.py +0 -0
- {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.
|
|
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[
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
) ->
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
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>
|
|
@@ -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"
|