From ac15542c0067d9f1280a18d3b2cf96f007bf1d6c Mon Sep 17 00:00:00 2001 From: WorldlineAcquiring Date: Fri, 20 Sep 2024 11:08:38 +0200 Subject: [PATCH] Release 0.1.0. --- .editorconfig | 12 + .gitattributes | 4 + .github/workflows/api-docs.yml | 59 ++ .github/workflows/deploy.yml | 32 + .gitignore | 54 ++ LICENSE.txt | 22 + MANIFEST.in | 2 + README.md | 2 - README.rst | 106 +++ conf.py | 359 ++++++++ index.rst | 789 ++++++++++++++++++ setup.cfg | 2 + setup.py | 71 ++ tests/__init__.py | 0 tests/file_utils.py | 24 + tests/integration/__init__.py | 0 tests/integration/init_utils.py | 172 ++++ tests/integration/test_connection_pooling.py | 74 ++ tests/integration/test_multipart_form_data.py | 181 ++++ tests/integration/test_nonexistent_proxy.py | 33 + tests/integration/test_process_payment.py | 29 + tests/integration/test_request_dcc_rate.py | 17 + tests/integration/test_sdk_proxy.py | 16 + .../oauth2AccessToken.expired.json | 4 + .../oauth2AccessToken.invalidClient.json | 4 + .../authentication/oauth2AccessToken.json | 4 + .../deleteWithVoidResponse.request | 4 + .../deleteWithVoidResponse.response | 5 + tests/resources/communication/generic.error | 1 + .../communication/getWithQueryParams.json | 3 + .../communication/getWithQueryParams.request | 4 + .../communication/getWithQueryParams.response | 7 + .../communication/getWithoutQueryParams.json | 3 + .../getWithoutQueryParams.request | 4 + .../getWithoutQueryParams.response | 7 + tests/resources/communication/notFound.html | 1 + .../resources/communication/notFound.response | 5 + .../postWithBadRequestResponse.json | 11 + .../postWithBadRequestResponse.request | 6 + .../postWithBadRequestResponse.response | 15 + .../postWithCreatedResponse.json | 10 + .../postWithCreatedResponse.request | 6 + .../postWithCreatedResponse.response | 10 + .../communication/unknownServerError.json | 10 + .../communication/unknownServerError.response | 14 + tests/resources/configuration.oauth2.ini | 9 + tests/resources/configuration.proxy.ini | 14 + tests/resources/log/bodyNoObfuscation.json | 7 + .../resources/log/bodyWithBinObfuscated.json | 3 + tests/resources/log/bodyWithBinOriginal.json | 3 + .../log/bodyWithCardCustomObfuscated.json | 13 + .../resources/log/bodyWithCardObfuscated.json | 13 + tests/resources/log/bodyWithCardOriginal.json | 13 + .../log/bodyWithObjectObfuscated.json | 5 + .../resources/log/bodyWithObjectOriginal.json | 5 + tests/run_all_tests.py | 24 + tests/run_integration_tests.py | 21 + tests/run_unit_tests.py | 21 + tests/unit/__init__.py | 0 tests/unit/comparable_param.py | 28 + tests/unit/mock_authenticator.py | 11 + tests/unit/server_mock_utils.py | 71 ++ tests/unit/test_body_obfuscator.py | 87 ++ tests/unit/test_client.py | 83 ++ tests/unit/test_communicator.py | 56 ++ tests/unit/test_communicator_configuration.py | 153 ++++ tests/unit/test_data_object.py | 74 ++ tests/unit/test_default_connection.py | 152 ++++ tests/unit/test_default_connection_logger.py | 648 ++++++++++++++ tests/unit/test_default_marshaller.py | 126 +++ tests/unit/test_factory.py | 74 ++ tests/unit/test_header_obfuscator.py | 68 ++ tests/unit/test_metadata_provider.py | 89 ++ tests/unit/test_oauth2_authenticator.py | 93 +++ .../unit/test_python_communication_logger.py | 63 ++ tests/unit/test_request_header.py | 46 + tests/unit/test_response_header.py | 55 ++ tests/unit/test_sysout_communicator_logger.py | 51 ++ worldline/__init__.py | 0 worldline/acquiring/__init__.py | 0 worldline/acquiring/sdk/__init__.py | 0 worldline/acquiring/sdk/api_resource.py | 42 + .../acquiring/sdk/authentication/__init__.py | 0 .../sdk/authentication/authenticator.py | 24 + .../sdk/authentication/authorization_type.py | 10 + .../authentication/oauth2_authenticator.py | 116 +++ .../sdk/authentication/oauth2_exception.py | 10 + worldline/acquiring/sdk/call_context.py | 8 + worldline/acquiring/sdk/client.py | 74 ++ .../acquiring/sdk/communication/__init__.py | 0 .../communication/communication_exception.py | 8 + .../acquiring/sdk/communication/connection.py | 84 ++ .../sdk/communication/default_connection.py | 301 +++++++ .../sdk/communication/metadata_provider.py | 131 +++ .../multipart_form_data_object.py | 51 ++ .../multipart_form_data_request.py | 16 + .../sdk/communication/not_found_exception.py | 10 + .../sdk/communication/param_request.py | 17 + .../sdk/communication/pooled_connection.py | 29 + .../sdk/communication/request_header.py | 60 ++ .../sdk/communication/request_param.py | 30 + .../sdk/communication/response_exception.py | 58 ++ .../sdk/communication/response_header.py | 50 ++ worldline/acquiring/sdk/communicator.py | 450 ++++++++++ .../sdk/communicator_configuration.py | 308 +++++++ worldline/acquiring/sdk/domain/__init__.py | 0 worldline/acquiring/sdk/domain/data_object.py | 34 + .../sdk/domain/shopping_cart_extension.py | 77 ++ .../acquiring/sdk/domain/uploadable_file.py | 51 ++ worldline/acquiring/sdk/factory.py | 124 +++ worldline/acquiring/sdk/json/__init__.py | 0 .../acquiring/sdk/json/default_marshaller.py | 60 ++ worldline/acquiring/sdk/json/marshaller.py | 32 + .../sdk/json/marshaller_syntax_exception.py | 13 + worldline/acquiring/sdk/log/__init__.py | 0 .../acquiring/sdk/log/body_obfuscator.py | 100 +++ .../acquiring/sdk/log/communicator_logger.py | 35 + .../acquiring/sdk/log/header_obfuscator.py | 47 ++ worldline/acquiring/sdk/log/log_message.py | 94 +++ .../acquiring/sdk/log/logging_capable.py | 26 + .../acquiring/sdk/log/obfuscation_capable.py | 24 + .../acquiring/sdk/log/obfuscation_rule.py | 50 ++ .../sdk/log/python_communicator_logger.py | 54 ++ .../acquiring/sdk/log/request_log_message.py | 28 + .../acquiring/sdk/log/response_log_message.py | 37 + .../sdk/log/sys_out_communicator_logger.py | 36 + .../acquiring/sdk/proxy_configuration.py | 102 +++ worldline/acquiring/sdk/v1/__init__.py | 4 + .../acquiring/sdk/v1/acquirer/__init__.py | 4 + .../sdk/v1/acquirer/acquirer_client.py | 33 + .../sdk/v1/acquirer/merchant/__init__.py | 4 + .../merchant/accountverifications/__init__.py | 4 + .../account_verifications_client.py | 59 ++ .../dynamiccurrencyconversion/__init__.py | 4 + .../dynamic_currency_conversion_client.py | 59 ++ .../v1/acquirer/merchant/merchant_client.py | 65 ++ .../v1/acquirer/merchant/payments/__init__.py | 4 + .../payments/get_payment_status_params.py | 40 + .../merchant/payments/payments_client.py | 254 ++++++ .../v1/acquirer/merchant/refunds/__init__.py | 4 + .../merchant/refunds/get_refund_params.py | 40 + .../merchant/refunds/refunds_client.py | 175 ++++ .../merchant/technicalreversals/__init__.py | 4 + .../technical_reversals_client.py | 63 ++ worldline/acquiring/sdk/v1/api_exception.py | 79 ++ .../sdk/v1/authorization_exception.py | 17 + worldline/acquiring/sdk/v1/domain/__init__.py | 4 + .../v1/domain/address_verification_data.py | 55 ++ .../acquiring/sdk/v1/domain/amount_data.py | 73 ++ .../api_account_verification_request.py | 126 +++ .../api_account_verification_response.py | 172 ++++ .../sdk/v1/domain/api_action_response.py | 134 +++ .../domain/api_action_response_for_refund.py | 134 +++ .../sdk/v1/domain/api_capture_request.py | 142 ++++ .../domain/api_capture_request_for_refund.py | 59 ++ .../sdk/v1/domain/api_increment_request.py | 105 +++ .../sdk/v1/domain/api_increment_response.py | 59 ++ .../v1/domain/api_payment_error_response.py | 114 +++ .../v1/domain/api_payment_refund_request.py | 145 ++++ .../sdk/v1/domain/api_payment_request.py | 189 +++++ .../sdk/v1/domain/api_payment_resource.py | 203 +++++ .../sdk/v1/domain/api_payment_response.py | 270 ++++++ .../v1/domain/api_payment_reversal_request.py | 105 +++ .../api_payment_summary_for_response.py | 118 +++ .../v1/domain/api_references_for_responses.py | 73 ++ .../sdk/v1/domain/api_refund_request.py | 171 ++++ .../sdk/v1/domain/api_refund_resource.py | 221 +++++ .../sdk/v1/domain/api_refund_response.py | 288 +++++++ .../domain/api_refund_summary_for_response.py | 118 +++ .../sdk/v1/domain/api_reversal_response.py | 41 + .../domain/api_technical_reversal_request.py | 77 ++ .../domain/api_technical_reversal_response.py | 111 +++ .../sdk/v1/domain/card_data_for_dcc.py | 73 ++ .../sdk/v1/domain/card_on_file_data.py | 78 ++ .../sdk/v1/domain/card_payment_data.py | 234 ++++++ .../v1/domain/card_payment_data_for_refund.py | 156 ++++ .../domain/card_payment_data_for_resource.py | 60 ++ .../domain/card_payment_data_for_response.py | 80 ++ .../card_payment_data_for_verification.py | 176 ++++ worldline/acquiring/sdk/v1/domain/dcc_data.py | 92 ++ .../acquiring/sdk/v1/domain/dcc_proposal.py | 101 +++ .../sdk/v1/domain/e_commerce_data.py | 83 ++ ..._commerce_data_for_account_verification.py | 65 ++ .../v1/domain/e_commerce_data_for_response.py | 55 ++ .../sdk/v1/domain/get_dcc_rate_request.py | 135 +++ .../sdk/v1/domain/get_dcc_rate_response.py | 94 +++ .../v1/domain/initial_card_on_file_data.py | 55 ++ .../acquiring/sdk/v1/domain/merchant_data.py | 145 ++++ .../sdk/v1/domain/network_token_data.py | 57 ++ .../sdk/v1/domain/payment_references.py | 75 ++ .../sdk/v1/domain/plain_card_data.py | 75 ++ .../sdk/v1/domain/point_of_sale_data.py | 37 + .../v1/domain/point_of_sale_data_for_dcc.py | 55 ++ .../acquiring/sdk/v1/domain/rate_data.py | 110 +++ .../acquiring/sdk/v1/domain/sub_operation.py | 192 +++++ .../sdk/v1/domain/sub_operation_for_refund.py | 174 ++++ .../v1/domain/subsequent_card_on_file_data.py | 73 ++ .../acquiring/sdk/v1/domain/three_d_secure.py | 111 +++ .../sdk/v1/domain/transaction_data_for_dcc.py | 79 ++ .../acquiring/sdk/v1/exception_factory.py | 38 + worldline/acquiring/sdk/v1/ping/__init__.py | 4 + .../acquiring/sdk/v1/ping/ping_client.py | 55 ++ .../acquiring/sdk/v1/platform_exception.py | 17 + .../acquiring/sdk/v1/reference_exception.py | 17 + worldline/acquiring/sdk/v1/v1_client.py | 43 + .../acquiring/sdk/v1/validation_exception.py | 17 + 206 files changed, 14420 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/api-docs.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.rst create mode 100644 conf.py create mode 100644 index.rst create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/file_utils.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/init_utils.py create mode 100644 tests/integration/test_connection_pooling.py create mode 100644 tests/integration/test_multipart_form_data.py create mode 100644 tests/integration/test_nonexistent_proxy.py create mode 100644 tests/integration/test_process_payment.py create mode 100644 tests/integration/test_request_dcc_rate.py create mode 100644 tests/integration/test_sdk_proxy.py create mode 100644 tests/resources/authentication/oauth2AccessToken.expired.json create mode 100644 tests/resources/authentication/oauth2AccessToken.invalidClient.json create mode 100644 tests/resources/authentication/oauth2AccessToken.json create mode 100644 tests/resources/communication/deleteWithVoidResponse.request create mode 100644 tests/resources/communication/deleteWithVoidResponse.response create mode 100644 tests/resources/communication/generic.error create mode 100644 tests/resources/communication/getWithQueryParams.json create mode 100644 tests/resources/communication/getWithQueryParams.request create mode 100644 tests/resources/communication/getWithQueryParams.response create mode 100644 tests/resources/communication/getWithoutQueryParams.json create mode 100644 tests/resources/communication/getWithoutQueryParams.request create mode 100644 tests/resources/communication/getWithoutQueryParams.response create mode 100644 tests/resources/communication/notFound.html create mode 100644 tests/resources/communication/notFound.response create mode 100644 tests/resources/communication/postWithBadRequestResponse.json create mode 100644 tests/resources/communication/postWithBadRequestResponse.request create mode 100644 tests/resources/communication/postWithBadRequestResponse.response create mode 100644 tests/resources/communication/postWithCreatedResponse.json create mode 100644 tests/resources/communication/postWithCreatedResponse.request create mode 100644 tests/resources/communication/postWithCreatedResponse.response create mode 100644 tests/resources/communication/unknownServerError.json create mode 100644 tests/resources/communication/unknownServerError.response create mode 100644 tests/resources/configuration.oauth2.ini create mode 100644 tests/resources/configuration.proxy.ini create mode 100644 tests/resources/log/bodyNoObfuscation.json create mode 100644 tests/resources/log/bodyWithBinObfuscated.json create mode 100644 tests/resources/log/bodyWithBinOriginal.json create mode 100644 tests/resources/log/bodyWithCardCustomObfuscated.json create mode 100644 tests/resources/log/bodyWithCardObfuscated.json create mode 100644 tests/resources/log/bodyWithCardOriginal.json create mode 100644 tests/resources/log/bodyWithObjectObfuscated.json create mode 100644 tests/resources/log/bodyWithObjectOriginal.json create mode 100644 tests/run_all_tests.py create mode 100644 tests/run_integration_tests.py create mode 100644 tests/run_unit_tests.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/comparable_param.py create mode 100644 tests/unit/mock_authenticator.py create mode 100644 tests/unit/server_mock_utils.py create mode 100644 tests/unit/test_body_obfuscator.py create mode 100644 tests/unit/test_client.py create mode 100644 tests/unit/test_communicator.py create mode 100644 tests/unit/test_communicator_configuration.py create mode 100644 tests/unit/test_data_object.py create mode 100644 tests/unit/test_default_connection.py create mode 100644 tests/unit/test_default_connection_logger.py create mode 100644 tests/unit/test_default_marshaller.py create mode 100644 tests/unit/test_factory.py create mode 100644 tests/unit/test_header_obfuscator.py create mode 100644 tests/unit/test_metadata_provider.py create mode 100644 tests/unit/test_oauth2_authenticator.py create mode 100644 tests/unit/test_python_communication_logger.py create mode 100644 tests/unit/test_request_header.py create mode 100644 tests/unit/test_response_header.py create mode 100644 tests/unit/test_sysout_communicator_logger.py create mode 100644 worldline/__init__.py create mode 100644 worldline/acquiring/__init__.py create mode 100644 worldline/acquiring/sdk/__init__.py create mode 100644 worldline/acquiring/sdk/api_resource.py create mode 100644 worldline/acquiring/sdk/authentication/__init__.py create mode 100644 worldline/acquiring/sdk/authentication/authenticator.py create mode 100644 worldline/acquiring/sdk/authentication/authorization_type.py create mode 100644 worldline/acquiring/sdk/authentication/oauth2_authenticator.py create mode 100644 worldline/acquiring/sdk/authentication/oauth2_exception.py create mode 100644 worldline/acquiring/sdk/call_context.py create mode 100644 worldline/acquiring/sdk/client.py create mode 100644 worldline/acquiring/sdk/communication/__init__.py create mode 100644 worldline/acquiring/sdk/communication/communication_exception.py create mode 100644 worldline/acquiring/sdk/communication/connection.py create mode 100644 worldline/acquiring/sdk/communication/default_connection.py create mode 100644 worldline/acquiring/sdk/communication/metadata_provider.py create mode 100644 worldline/acquiring/sdk/communication/multipart_form_data_object.py create mode 100644 worldline/acquiring/sdk/communication/multipart_form_data_request.py create mode 100644 worldline/acquiring/sdk/communication/not_found_exception.py create mode 100644 worldline/acquiring/sdk/communication/param_request.py create mode 100644 worldline/acquiring/sdk/communication/pooled_connection.py create mode 100644 worldline/acquiring/sdk/communication/request_header.py create mode 100644 worldline/acquiring/sdk/communication/request_param.py create mode 100644 worldline/acquiring/sdk/communication/response_exception.py create mode 100644 worldline/acquiring/sdk/communication/response_header.py create mode 100644 worldline/acquiring/sdk/communicator.py create mode 100644 worldline/acquiring/sdk/communicator_configuration.py create mode 100644 worldline/acquiring/sdk/domain/__init__.py create mode 100644 worldline/acquiring/sdk/domain/data_object.py create mode 100644 worldline/acquiring/sdk/domain/shopping_cart_extension.py create mode 100644 worldline/acquiring/sdk/domain/uploadable_file.py create mode 100644 worldline/acquiring/sdk/factory.py create mode 100644 worldline/acquiring/sdk/json/__init__.py create mode 100644 worldline/acquiring/sdk/json/default_marshaller.py create mode 100644 worldline/acquiring/sdk/json/marshaller.py create mode 100644 worldline/acquiring/sdk/json/marshaller_syntax_exception.py create mode 100644 worldline/acquiring/sdk/log/__init__.py create mode 100644 worldline/acquiring/sdk/log/body_obfuscator.py create mode 100644 worldline/acquiring/sdk/log/communicator_logger.py create mode 100644 worldline/acquiring/sdk/log/header_obfuscator.py create mode 100644 worldline/acquiring/sdk/log/log_message.py create mode 100644 worldline/acquiring/sdk/log/logging_capable.py create mode 100644 worldline/acquiring/sdk/log/obfuscation_capable.py create mode 100644 worldline/acquiring/sdk/log/obfuscation_rule.py create mode 100644 worldline/acquiring/sdk/log/python_communicator_logger.py create mode 100644 worldline/acquiring/sdk/log/request_log_message.py create mode 100644 worldline/acquiring/sdk/log/response_log_message.py create mode 100644 worldline/acquiring/sdk/log/sys_out_communicator_logger.py create mode 100644 worldline/acquiring/sdk/proxy_configuration.py create mode 100644 worldline/acquiring/sdk/v1/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/acquirer_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/payments/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/refunds/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/__init__.py create mode 100644 worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.py create mode 100644 worldline/acquiring/sdk/v1/api_exception.py create mode 100644 worldline/acquiring/sdk/v1/authorization_exception.py create mode 100644 worldline/acquiring/sdk/v1/domain/__init__.py create mode 100644 worldline/acquiring/sdk/v1/domain/address_verification_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/amount_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_account_verification_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_account_verification_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_action_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_capture_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_increment_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_increment_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_error_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_refund_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_resource.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_references_for_responses.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_refund_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_refund_resource.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_refund_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_reversal_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_data_for_dcc.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_on_file_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_payment_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.py create mode 100644 worldline/acquiring/sdk/v1/domain/dcc_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/dcc_proposal.py create mode 100644 worldline/acquiring/sdk/v1/domain/e_commerce_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.py create mode 100644 worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.py create mode 100644 worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.py create mode 100644 worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/merchant_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/network_token_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/payment_references.py create mode 100644 worldline/acquiring/sdk/v1/domain/plain_card_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/point_of_sale_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.py create mode 100644 worldline/acquiring/sdk/v1/domain/rate_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/sub_operation.py create mode 100644 worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.py create mode 100644 worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.py create mode 100644 worldline/acquiring/sdk/v1/domain/three_d_secure.py create mode 100644 worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.py create mode 100644 worldline/acquiring/sdk/v1/exception_factory.py create mode 100644 worldline/acquiring/sdk/v1/ping/__init__.py create mode 100644 worldline/acquiring/sdk/v1/ping/ping_client.py create mode 100644 worldline/acquiring/sdk/v1/platform_exception.py create mode 100644 worldline/acquiring/sdk/v1/reference_exception.py create mode 100644 worldline/acquiring/sdk/v1/v1_client.py create mode 100644 worldline/acquiring/sdk/v1/validation_exception.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80489cb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rst] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..59e5462 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +.gitattributes export-ignore +.gitignore export-ignore + +* text eol=lf diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml new file mode 100644 index 0000000..8bf778f --- /dev/null +++ b/.github/workflows/api-docs.yml @@ -0,0 +1,59 @@ +name: API docs + +on: + push: + tags: ['[0-9]+.[0-9]+*'] + +permissions: + contents: write + +jobs: + api-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + path: code + persist-credentials: false + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install Sphinx and dependencies + run: pip install sphinx requests==2.* requests-toolbelt==0.* appengine-python-standard + - name: Build API docs + run: sphinx-build . docs + working-directory: code + - name: Checkout pages + uses: actions/checkout@v4 + with: + ref: gh-pages + path: pages + - name: Deploy pages + run: | + SDK_VERSION_FOLDER=`echo "$SDK_VERSION" | awk --field-separator '.' '{print $1".x";}'` + + # Create .nojekyll if it doesn't exist yet + touch .nojekyll + + mkdir -p "$SDK_VERSION_FOLDER" + rsync --quiet --archive --checksum --delete --exclude .git ../code/docs/ "$SDK_VERSION_FOLDER/" + # Remove .buildinfo and .doctrees generated by Sphinx + if [ -f "$SDK_VERSION_FOLDER/.buildinfo" ]; then rm "$SDK_VERSION_FOLDER/.buildinfo"; fi + if [ -d "$SDK_VERSION_FOLDER/.doctrees" ]; then rm -r "$SDK_VERSION_FOLDER/.doctrees"; fi + if [ -e latest ]; then rm -r latest; fi + ln -s "$SDK_VERSION_FOLDER" latest + + git config user.email "$USER_EMAIL" + git config user.name "$USER_NAME" + git add --all . + # Only commit when there are changes + git diff --quiet && git diff --staged --quiet || git commit --message "Generated API docs for version ${SDK_VERSION}" + git push + shell: bash + working-directory: pages + env: + SDK_VERSION: ${{ github.ref_name }} + USER_EMAIL: ${{ github.event.pusher.email }} + USER_NAME: ${{ github.event.pusher.name }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..6a032ad --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,32 @@ +name: Deploy + +on: + push: + tags: ['[0-9]+.[0-9]+*'] + +env: + SDK_VERSION: ${{ github.ref_name }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install Twine and dependencies + run: pip install twine requests==2.* requests-toolbelt==0.* appengine-python-standard + - name: Build + run: python setup.py sdist --formats zip --dist-dir . + - name: Verify + run: twine check "acquiring-sdk-python-${SDK_VERSION}.zip" + - name: Deploy + run: twine upload --username "${PYPI_USERNAME}" --password "${PYPI_PASSWORD}" "acquiring-sdk-python-${SDK_VERSION}.zip" + env: + PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8263d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +.idea/ +.settings/ +.project +.pydevproject +.vscode/ +main_test.py + +#python gitignote template from github +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1808b48 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2024 Worldline Financial Services (Europe) SA + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..926cc7b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst +include LICENSE.txt diff --git a/README.md b/README.md deleted file mode 100644 index 7d337ed..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# acquiring-sdk-python -Worldline Acquiring Python Server SDK diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c15d289 --- /dev/null +++ b/README.rst @@ -0,0 +1,106 @@ +Worldline Acquiring Python SDK +============================== + +Introduction +------------ + +The Python SDK helps you to communicate with the Worldline Acquiring API. Its primary features are: + +- convenient Python library for the API calls and responses + + - marshals Python request objects to HTTP requests + - unmarshals HTTP responses to Python response objects or Python exceptions + +- handling of all the details concerning authentication +- handling of required metadata + +See the `Worldline Acquiring Documentation `__ for more information on how to use the SDK. + +Structure of this repository +---------------------------- + +This repository consists out of four main components: + +#. The source code of the SDK itself: ``/worldline/acquiring/sdk/`` +#. The source code of the SDK unit tests: ``/tests/unit/`` +#. The source code of the SDK integration tests: ``/tests/integration/`` + +Note that the source code of the unit tests and integration tests can only be found on GitHub. + +Requirements +------------ + +Python 3.7 or higher is required. In addition, the following packages are required: + +- `requests `__ 2.25.0 or higher +- `requests-toolbelt `__ 0.8.0 or higher + +These packages will be installed automatically if the SDK is installed manually or using pip following the below instructions. + +Installation +------------ + +To install the SDK using pip, execute the following command: + +.. code:: bash + + pip install acquiring-sdk-python + +Alternatively, you can install the SDK from a source distribution file: + +#. Download the latest version of the Python SDK from GitHub. Choose the ``acquiring-sdk-python-x.y.z.zip`` file from the `releases `__ page, where ``x.y.z`` is the version number. + +#. Execute the following command in the folder where the SDK was downloaded to: + + .. code:: bash + + pip install acquiring-sdk-python-x.y.z.zip + +Uninstalling +------------ + +After the Python SDK has been installed, it can be uninstalled using the following command: + +.. code:: bash + + pip uninstall acquiring-sdk-python + +The required packages can be uninstalled in the same way. + +Running tests +------------- + +There are two types of tests: unit tests and integration tests. The unit tests will work out-of-the-box; for the integration tests some configuration is required. +First, some environment variables need to be set: + +- ``acquiring.api.oauth2.clientId`` for the OAUth2 client id to use. +- ``acquiring.api.oauth2.clientSecret`` for the OAuth2 client secret to use. +- ``acquiring.api.merchantId`` for your merchant ID. + +In addition, to run the proxy integration tests, the proxy URI, username and password should be set in the ``tests/resources/configuration.proxy.ini`` file. + +In order to run the unit and integration tests, the `mock `__ backport and `mockito `__ are required. These can be installed using the following command: + +.. code:: bash + + pip install mock mockito + +The following commands can now be executed from the ``tests`` directory to execute the tests: + +- Unit tests: + + .. code:: bash + + python run_unit_tests.py + +- Integration tests: + + .. code:: bash + + python run_integration_tests.py + +- Both unit and integration tests: + + .. code:: bash + + python run_all_tests.py diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..dd8d459 --- /dev/null +++ b/conf.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# +# test documentation build configuration file. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'acquiring-sdk-python' +copyright = 'Copyright (c) 2024 Worldline Financial Services (Europe) SA' +author = 'Worldline Acquiring' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.0' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'Python SDK v0.1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'acquiring-sdk-pythondoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'acquiring-sdk-python.tex', 'acquiring-sdk-python Documentation', + author, 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'acquiring-sdk-python', 'acquiring-sdk-python Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +# texinfo_documents = [ +# (master_doc, 'test', 'Python SDK Documentation', +# author, 'test', +# 'SDK to communicate with the Worldline Acquiring platform using the Worldline Acquiring API', +# 'Miscellaneous'), +# ] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False + +def filter_members(app, what, name, obj, skip, options): + # include __init__ and __str__ methods that are documented + if name in ('__init__', '__str__') and not skip: + return not bool(obj.__doc__) + # exclude nested classes + if what == 'class' and str(obj)[:7] == '= 2.25.0", + "requests-toolbelt >= 0.8.0" + ], + # test_suite="tests/run_unit_tests" # enables command 'pip acquiring-sdk-python test', which runs unit tests) + + # setup_requires=[ # setuptools_scm automatically reads the version from version control + # 'setuptools_scm' + # ], + # use_scm_version=True # turns setuptools_scm on +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/file_utils.py b/tests/file_utils.py new file mode 100644 index 0000000..efaf473 --- /dev/null +++ b/tests/file_utils.py @@ -0,0 +1,24 @@ +"""Defines functions to read contents from a file and close the file automatically, +if the relative path between this file and the resources folder is changed, this file should be updated""" +import os + + +def read_file(rel_path, *args, **kwargs): + """Function to read the contents of a file located in the subfolder tests/resources into a string + + :param rel_path: the relative path to the file from tests/resources""" + path = os.path.join(os.path.dirname(__file__), "resources", rel_path) + with open(path, *args, **kwargs) as _file: + return _file.read() + + +def write_file(rel_path, text, *args, **kwargs): + """Function to write text to a file located in the subfolder tests/resources into a string + + :param rel_path: the relative path to the file from tests/resources + :param text: text to write to the file + :param args: arguments to delegate to the builtin open + :param kwargs: arguments to delegate to the builtin open""" + path = os.path.join(os.path.dirname(__file__), "resources", rel_path) + with open(path, 'w+', *args, **kwargs) as _file: + _file.write(text) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/init_utils.py b/tests/integration/init_utils.py new file mode 100644 index 0000000..e7f73f1 --- /dev/null +++ b/tests/integration/init_utils.py @@ -0,0 +1,172 @@ +import os +from configparser import ConfigParser +from datetime import datetime +from uuid import uuid4 + +from worldline.acquiring.sdk.authentication.oauth2_authenticator import OAuth2Authenticator +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.communicator import Communicator +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller +from worldline.acquiring.sdk.v1.domain.amount_data import AmountData +from worldline.acquiring.sdk.v1.domain.api_payment_request import ApiPaymentRequest +from worldline.acquiring.sdk.v1.domain.api_payment_resource import ApiPaymentResource +from worldline.acquiring.sdk.v1.domain.api_payment_response import ApiPaymentResponse +from worldline.acquiring.sdk.v1.domain.card_data_for_dcc import CardDataForDcc +from worldline.acquiring.sdk.v1.domain.card_payment_data import CardPaymentData +from worldline.acquiring.sdk.v1.domain.e_commerce_data import ECommerceData +from worldline.acquiring.sdk.v1.domain.get_dcc_rate_request import GetDCCRateRequest +from worldline.acquiring.sdk.v1.domain.get_dcc_rate_response import GetDccRateResponse +from worldline.acquiring.sdk.v1.domain.payment_references import PaymentReferences +from worldline.acquiring.sdk.v1.domain.plain_card_data import PlainCardData +from worldline.acquiring.sdk.v1.domain.point_of_sale_data_for_dcc import PointOfSaleDataForDcc +from worldline.acquiring.sdk.v1.domain.transaction_data_for_dcc import TransactionDataForDcc + +"""File containing a number of creation methods for integration tests""" + +PROPERTIES_URL_OAUTH2 = os.path.abspath(os.path.join(__file__, os.pardir, "../resources/configuration.oauth2.ini")) +PROPERTIES_URL_PROXY = os.path.abspath(os.path.join(__file__, os.pardir, "../resources/configuration.proxy.ini")) +# OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET, OAUTH2_TOKEN_URI, MERCHANT_ID and ACQUIRER_ID are stored in OS and should be retrieved +OAUTH2_CLIENT_ID = os.getenv("acquiring.api.oauth2.clientId") +OAUTH2_CLIENT_SECRET = os.getenv("acquiring.api.oauth2.clientSecret") +OAUTH2_TOKEN_URI = os.getenv("acquiring.api.oauth2.tokenUri") +MERCHANT_ID = str(os.getenv("acquiring.api.merchantId")) +ACQUIRER_ID = str(os.getenv("acquiring.api.acquirerId")) +if OAUTH2_CLIENT_ID is None: + raise EnvironmentError("could not access environment variable acquiring.api.oauth2.clientId required for testing") +if OAUTH2_CLIENT_SECRET is None: + raise EnvironmentError("could not access environment variable acquiring.api.oauth2.clientSecret required for testing") +if OAUTH2_TOKEN_URI is None: + raise EnvironmentError("could not access environment variable acquiring.api.oauth2.tokenUri required for testing") +if MERCHANT_ID == 'None': + raise EnvironmentError("could not access environment variable acquiring.api.merchantId required for testing") +if ACQUIRER_ID == 'None': + raise EnvironmentError("could not access environment variable acquiring.api.acquirerId required for testing") + + +def create_communicator_configuration(properties_url=PROPERTIES_URL_OAUTH2, max_connections=None): + """Convenience method to create a communicator configuration that connects to a host stored in system variables""" + try: + parser = ConfigParser() + parser.read(properties_url) + with open(properties_url) as f: + parser.read_file(f) + configuration = CommunicatorConfiguration(parser, + oauth2_client_id=OAUTH2_CLIENT_ID, + oauth2_client_secret=OAUTH2_CLIENT_SECRET, + oauth2_token_uri=OAUTH2_TOKEN_URI, + max_connections=max_connections) + except IOError as e: + raise RuntimeError("Unable to read configuration", e) + host = os.getenv("acquiring.api.endpoint.host") + if host is not None: + scheme = os.getenv("acquiring.api.endpoint.scheme", "https") + port = int(os.getenv("acquiring.api.endpoint.port", -1)) + configuration.api_endpoint = "{2}://{0}:{1}".format(host, port, scheme) + return configuration + + +def create_communicator(): + configuration = create_communicator_configuration() + authenticator = OAuth2Authenticator(configuration) + return Communicator(api_endpoint=configuration.api_endpoint, authenticator=authenticator, + connection=DefaultConnection(3, 3), metadata_provider=MetadataProvider("Worldline"), + marshaller=DefaultMarshaller.instance()) + + +def create_client(max_connections=None): + configuration = create_communicator_configuration(max_connections=max_connections) + return Factory.create_client_from_configuration(configuration) + + +def create_client_with_proxy(max_connections=None): + configuration = create_communicator_configuration(PROPERTIES_URL_PROXY, max_connections=max_connections) + return Factory.create_client_from_configuration(configuration) + + +def get_api_payment_request(): + request = ApiPaymentRequest() + + request.amount = AmountData() + request.amount.amount = 200 + request.amount.currency_code = "GBP" + request.amount.number_of_decimals = 2 + request.authorization_type = "PRE_AUTHORIZATION" + request.transaction_timestamp = datetime.now() + request.card_payment_data = CardPaymentData() + request.card_payment_data.card_entry_mode = "ECOMMERCE" + request.card_payment_data.allow_partial_approval = False + request.card_payment_data.brand = "VISA" + request.card_payment_data.capture_immediately = False + request.card_payment_data.cardholder_verification_method = "CARD_SECURITY_CODE" + request.card_payment_data.card_data = PlainCardData() + request.card_payment_data.card_data.expiry_date = "122031" + request.card_payment_data.card_data.card_number = "4176669999000104" + request.card_payment_data.card_data.card_security_code = "012" + request.references = PaymentReferences() + request.references.merchant_reference = "your-order-" + str(uuid4()) + request.operation_id = str(uuid4()) + return request + + +def assert_payment_response(self, request: ApiPaymentRequest, response: ApiPaymentResponse): + self.assertEqual(request.operation_id, response.operation_id) + self.assertEqual("0", response.response_code) + self.assertEqual("APPROVED", response.response_code_category) + self.assertIsNotNone(response.response_code_description) + self.assertEqual("AUTHORIZED", response.status) + self.assertIsNotNone(response.initial_authorization_code) + self.assertIsNotNone(response.payment_id) + self.assertIsNotNone(response.total_authorized_amount) + self.assertEqual(200, response.total_authorized_amount.amount) + self.assertEqual("GBP", response.total_authorized_amount.currency_code) + self.assertEqual(2, response.total_authorized_amount.number_of_decimals) + + +def assert_payment_status_response(self, payment_id: str, response: ApiPaymentResource): + self.assertIsNotNone(response.initial_authorization_code) + self.assertEqual(payment_id, response.payment_id) + self.assertEqual("AUTHORIZED", response.status) + + +def get_dcc_rate_request(amount: int = 200): + amount_data = AmountData() + amount_data.amount = amount + amount_data.currency_code = "GBP" + amount_data.number_of_decimals = 2 + + transaction_data_for_dcc = TransactionDataForDcc() + transaction_data_for_dcc.amount = amount_data + transaction_data_for_dcc.transaction_type = "PAYMENT" + transaction_data_for_dcc.transaction_timestamp = datetime.now() + + point_of_sale_data_for_dcc = PointOfSaleDataForDcc() + point_of_sale_data_for_dcc.terminal_id = "12345678" + + card_data_for_dcc = CardDataForDcc() + card_data_for_dcc.bin = "41766699" + card_data_for_dcc.brand = "VISA" + + request = GetDCCRateRequest() + request.operation_id = str(uuid4()) + request.target_currency = "EUR" + request.card_payment_data = card_data_for_dcc + request.point_of_sale_data = point_of_sale_data_for_dcc + request.transaction = transaction_data_for_dcc + + return request + + +def assert_dcc_rate_response(self, request: GetDCCRateRequest, response: GetDccRateResponse): + self.assertIsNotNone(response.proposal) + self.assertIsNotNone(response.proposal.original_amount) + assert_equal_amounts(self, request.transaction.amount, response.proposal.original_amount) + self.assertEqual(request.target_currency, response.proposal.resulting_amount.currency_code) + + +def assert_equal_amounts(self, expected: AmountData, actual: AmountData): + self.assertEqual(expected.amount, actual.amount) + self.assertEqual(expected.currency_code, actual.currency_code) + self.assertEqual(expected.number_of_decimals, actual.number_of_decimals) diff --git a/tests/integration/test_connection_pooling.py b/tests/integration/test_connection_pooling.py new file mode 100644 index 0000000..68a88fe --- /dev/null +++ b/tests/integration/test_connection_pooling.py @@ -0,0 +1,74 @@ +import unittest +import threading +import timeit + +import tests.integration.init_utils as init_utils +from tests.integration.init_utils import ACQUIRER_ID, MERCHANT_ID + +from worldline.acquiring.sdk.factory import Factory + + +class ConnectionPoolingTest(unittest.TestCase): + """Performs multiple threaded server requests with connection pooling in order to test thread-safety and concurrency + """ + + def setUp(self): + self.flag = threading.Event() # flag to synchronise a start moment for the threads + self.result_list = [] # list to collect results from the threads + self.lock = threading.RLock() # mutex lock for the threads to provide concurrent access to the result list + + def test_connection_pool_max_is_count(self): + """Test with one pool per request""" + self.run_connection_pooling_test(10, 10) + + def test_connection_pool_max_is_half(self): + """Test with one pool per two requests""" + self.run_connection_pooling_test(10, 5) + + def test_connection_pool_max_is_one(self): + """Test with one pool for all 10 requests""" + self.run_connection_pooling_test(10, 1) + + def run_connection_pooling_test(self, request_count, max_connections): + """Sends *request_count* requests with a maximum number of connection pools equal to *max_connections*""" + communicator_configuration = init_utils.create_communicator_configuration(max_connections=max_connections) + + with Factory.create_communicator_from_configuration(communicator_configuration) as communicator: + # Create a number of runner threads that will execute send_request + runner_threads = [ + threading.Thread(target=self.send_request, args=(i, communicator)) for i in range(0, request_count) + ] + for thread in runner_threads: + thread.start() + self.flag.set() + + # wait until threads are done before closing the communicator + for i in range(0, request_count - 1): + runner_threads[i].join() + print("(*start time*, *end time*) for {} connection pools".format(max_connections)) + for item in self.result_list: + if isinstance(item, Exception): + self.fail("an exception occurred in one of the threads:/n" + str(item)) + else: + print(repr(item)) + # check server logs for information about concurrent use of connections + + def send_request(self, i, communicator): + """runs a (concurrent) request""" + request = init_utils.get_dcc_rate_request() + try: + client = Factory.create_client_from_communicator(communicator) + self.flag.wait() + start_time = timeit.default_timer() + client.v1().acquirer(ACQUIRER_ID).merchant(MERCHANT_ID).dynamic_currency_conversion().request_dcc_rate(request) + end_time = timeit.default_timer() + with self.lock: + self.result_list.append((start_time, end_time)) + except Exception as e: + with self.lock: + self.result_list.append(e) + # check server logs for additional data about the requests sent + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_multipart_form_data.py b/tests/integration/test_multipart_form_data.py new file mode 100644 index 0000000..97260d7 --- /dev/null +++ b/tests/integration/test_multipart_form_data.py @@ -0,0 +1,181 @@ +import json +import os +import unittest + +import tests.integration.init_utils as init_utils + +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.communication.multipart_form_data_object import MultipartFormDataObject +from worldline.acquiring.sdk.communication.multipart_form_data_request import MultipartFormDataRequest +from worldline.acquiring.sdk.domain.data_object import DataObject +from worldline.acquiring.sdk.domain.uploadable_file import UploadableFile + + +HTTPBIN_URL = os.getenv("httpbin.url") or 'http://httpbin.org' + + +class MultipartFormDataTest(unittest.TestCase): + """Test multipart/form-data uploads""" + + def test_multipart_form_data_upload_post_multipart_form_data_object_with_response(self): + """Test a multipart/form-data POST upload with a response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.post('/anything/operations', None, None, multipart, HttpBinResponse, None) + + self.assertEqual(response.form['value'], 'Hello World') + self.assertEqual(response.files['file'], 'file-content') + + def test_multipart_form_data_upload_post_multipart_form_data_request_with_response(self): + """Test a multipart/form-data POST upload with a response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.post('/anything/operations', None, None, MultipartFormDataObjectWrapper(multipart), HttpBinResponse, None) + + self.assertEqual(response.form['value'], 'Hello World') + self.assertEqual(response.files['file'], 'file-content') + + def test_multipart_form_data_upload_post_multipart_form_data_object_with_binary_response(self): + """Test a multipart/form-data POST upload with a binary response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.post_with_binary_response('/anything/operations', None, None, multipart, None) + + data = '' + for chunk in response[1]: + data += chunk.decode('utf-8') + response = json.loads(data) + self.assertEqual(response['form']['value'], 'Hello World') + self.assertEqual(response['files']['file'], 'file-content') + + def test_multipart_form_data_upload_post_multipart_form_data_request_with_binary_response(self): + """Test a multipart/form-data POST upload with a binary response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.post_with_binary_response('/anything/operations', None, None, MultipartFormDataObjectWrapper(multipart), None) + + data = '' + for chunk in response[1]: + data += chunk.decode('utf-8') + response = json.loads(data) + self.assertEqual(response['form']['value'], 'Hello World') + self.assertEqual(response['files']['file'], 'file-content') + + def test_multipart_form_data_upload_put_multipart_form_data_object_with_response(self): + """Test a multipart/form-data PUT upload with a response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.put('/anything/operations', None, None, multipart, HttpBinResponse, None) + + self.assertEqual(response.form['value'], 'Hello World') + self.assertEqual(response.files['file'], 'file-content') + + def test_multipart_form_data_upload_put_multipart_form_data_request_with_response(self): + """Test a multipart/form-data PUT upload with a response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.put('/anything/operations', None, None, MultipartFormDataObjectWrapper(multipart), HttpBinResponse, None) + + self.assertEqual(response.form['value'], 'Hello World') + self.assertEqual(response.files['file'], 'file-content') + + def test_multipart_form_data_upload_put_multipart_form_data_object_with_binary_response(self): + """Test a multipart/form-data PUT upload with a binary response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.put_with_binary_response('/anything/operations', None, None, multipart, None) + + data = '' + for chunk in response[1]: + data += chunk.decode('utf-8') + response = json.loads(data) + self.assertEqual(response['form']['value'], 'Hello World') + self.assertEqual(response['files']['file'], 'file-content') + + def test_multipart_form_data_upload_put_multipart_form_data_request_with_binary_response(self): + """Test a multipart/form-data PUT upload with a binary response""" + configuration = init_utils.create_communicator_configuration() + configuration.api_endpoint = HTTPBIN_URL + + multipart = MultipartFormDataObject() + multipart.add_file('file', UploadableFile('file.txt', 'file-content', 'text/plain')) + multipart.add_value('value', 'Hello World') + + with Factory.create_communicator_from_configuration(configuration) as communicator: + response = communicator.put_with_binary_response('/anything/operations', None, None, MultipartFormDataObjectWrapper(multipart), None) + + data = '' + for chunk in response[1]: + data += chunk.decode('utf-8') + response = json.loads(data) + self.assertEqual(response['form']['value'], 'Hello World') + self.assertEqual(response['files']['file'], 'file-content') + + +class HttpBinResponse(DataObject): + form = None + files = None + + def from_dictionary(self, dictionary): + super(HttpBinResponse, self).from_dictionary(dictionary) + if 'form' in dictionary: + self.form = dictionary['form'] + if 'files' in dictionary: + self.files = dictionary['files'] + return self + + +class MultipartFormDataObjectWrapper(MultipartFormDataRequest): + __multipart = None + + def __init__(self, multipart): + self.__multipart = multipart + + def to_multipart_form_data_object(self): + return self.__multipart + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_nonexistent_proxy.py b/tests/integration/test_nonexistent_proxy.py new file mode 100644 index 0000000..753c73c --- /dev/null +++ b/tests/integration/test_nonexistent_proxy.py @@ -0,0 +1,33 @@ +import unittest +import configparser + +import tests.integration.init_utils as init_utils +from tests.integration.init_utils import ACQUIRER_ID, MERCHANT_ID + +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.proxy_configuration import ProxyConfiguration +from worldline.acquiring.sdk.communication.communication_exception import CommunicationException + + +class ProxyTest(unittest.TestCase): + def test_connect_nonexistent_proxy(self): + """Try connecting to a nonexistent proxy and assert it fails to connect to it""" + parser = configparser.ConfigParser() + parser.read(init_utils.PROPERTIES_URL_PROXY) + communicator_config = CommunicatorConfiguration(parser, connect_timeout=1, socket_timeout=1, + oauth2_client_id=init_utils.OAUTH2_CLIENT_ID, + oauth2_client_secret=init_utils.OAUTH2_CLIENT_SECRET, + oauth2_token_uri=init_utils.OAUTH2_TOKEN_URI, + proxy_configuration=ProxyConfiguration( + host="localhost", port=65535, + username="arg", password="blarg") + ) + with Factory.create_client_from_configuration(communicator_config) as client: + with self.assertRaises(CommunicationException): + request = init_utils.get_dcc_rate_request() + client.v1().acquirer(ACQUIRER_ID).merchant(MERCHANT_ID).dynamic_currency_conversion().request_dcc_rate(request) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_process_payment.py b/tests/integration/test_process_payment.py new file mode 100644 index 0000000..84af5c6 --- /dev/null +++ b/tests/integration/test_process_payment.py @@ -0,0 +1,29 @@ +import unittest + +import tests.integration.init_utils as init_utils +from tests.integration.init_utils import ACQUIRER_ID, MERCHANT_ID + +from worldline.acquiring.sdk.v1.acquirer.merchant.payments.get_payment_status_params import GetPaymentStatusParams + + +class ProcessPaymentTest(unittest.TestCase): + def test_process_payment(self): + """Smoke test for process payment""" + with init_utils.create_client() as client: + payments_client = client.v1().acquirer(ACQUIRER_ID).merchant(MERCHANT_ID).payments() + + request = init_utils.get_api_payment_request() + response = payments_client.process_payment(request) + init_utils.assert_payment_response(self, request, response) + + payment_id = response.payment_id + + query = GetPaymentStatusParams() + query.return_operations = True + + status = payments_client.get_payment_status(payment_id, query) + init_utils.assert_payment_status_response(self, payment_id, status) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_request_dcc_rate.py b/tests/integration/test_request_dcc_rate.py new file mode 100644 index 0000000..04830a6 --- /dev/null +++ b/tests/integration/test_request_dcc_rate.py @@ -0,0 +1,17 @@ +import unittest + +import tests.integration.init_utils as init_utils +from tests.integration.init_utils import ACQUIRER_ID, MERCHANT_ID + + +class ProcessPaymentTest(unittest.TestCase): + def test_request_dcc_rate(self): + """Smoke test for request DCC rate""" + with init_utils.create_client() as client: + request = init_utils.get_dcc_rate_request() + response = client.v1().acquirer(ACQUIRER_ID).merchant(MERCHANT_ID).dynamic_currency_conversion().request_dcc_rate(request) + init_utils.assert_dcc_rate_response(self, request, response) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_sdk_proxy.py b/tests/integration/test_sdk_proxy.py new file mode 100644 index 0000000..b57b284 --- /dev/null +++ b/tests/integration/test_sdk_proxy.py @@ -0,0 +1,16 @@ +import unittest + +from tests.integration import init_utils +from tests.integration.init_utils import ACQUIRER_ID, MERCHANT_ID + + +class SDKProxyTest(unittest.TestCase): + def test_sdk_proxy(self): + with init_utils.create_client_with_proxy() as client: + request = init_utils.get_dcc_rate_request() + response = client.v1().acquirer(ACQUIRER_ID).merchant(MERCHANT_ID).dynamic_currency_conversion().request_dcc_rate(request) + init_utils.assert_dcc_rate_response(self, request, response) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/resources/authentication/oauth2AccessToken.expired.json b/tests/resources/authentication/oauth2AccessToken.expired.json new file mode 100644 index 0000000..23286ec --- /dev/null +++ b/tests/resources/authentication/oauth2AccessToken.expired.json @@ -0,0 +1,4 @@ +{ + "access_token": "expiredAccessToken", + "expires_in": -1 +} diff --git a/tests/resources/authentication/oauth2AccessToken.invalidClient.json b/tests/resources/authentication/oauth2AccessToken.invalidClient.json new file mode 100644 index 0000000..275cacd --- /dev/null +++ b/tests/resources/authentication/oauth2AccessToken.invalidClient.json @@ -0,0 +1,4 @@ +{ + "error": "unauthorized_client", + "error_description": "INVALID_CREDENTIALS: Invalid client credentials" +} diff --git a/tests/resources/authentication/oauth2AccessToken.json b/tests/resources/authentication/oauth2AccessToken.json new file mode 100644 index 0000000..641e3db --- /dev/null +++ b/tests/resources/authentication/oauth2AccessToken.json @@ -0,0 +1,4 @@ +{ + "access_token": "accessToken", + "expires_in": 300 +} diff --git a/tests/resources/communication/deleteWithVoidResponse.request b/tests/resources/communication/deleteWithVoidResponse.request new file mode 100644 index 0000000..12dcb8b --- /dev/null +++ b/tests/resources/communication/deleteWithVoidResponse.request @@ -0,0 +1,4 @@ +Outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\)\:\ +\ \ method\:\ \ \ \ \ \ \ \'DELETE\'\ +\ \ uri\:\ \ \ \ \ \ \ \ \ \ \'\/v1\/void\'\ +\ \ headers\:\ \ \ \ \ \ \'X\-WL\-ServerMetaInfo\=\"[^"]*\"\,\ Date\=\"[^"]+\"\,\ Authorization\=\"\*\*\*\*\*\*\*\*\"[^']*\' diff --git a/tests/resources/communication/deleteWithVoidResponse.response b/tests/resources/communication/deleteWithVoidResponse.response new file mode 100644 index 0000000..df919fc --- /dev/null +++ b/tests/resources/communication/deleteWithVoidResponse.response @@ -0,0 +1,5 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'204\'\ +\ \ headers\:\ \ \ \ \ \ \'Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\' \ No newline at end of file diff --git a/tests/resources/communication/generic.error b/tests/resources/communication/generic.error new file mode 100644 index 0000000..0999c12 --- /dev/null +++ b/tests/resources/communication/generic.error @@ -0,0 +1 @@ +Error\ occurred\ for\ outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\) \ No newline at end of file diff --git a/tests/resources/communication/getWithQueryParams.json b/tests/resources/communication/getWithQueryParams.json new file mode 100644 index 0000000..da6da99 --- /dev/null +++ b/tests/resources/communication/getWithQueryParams.json @@ -0,0 +1,3 @@ +{ + "convertedAmount" : 4547504 +} \ No newline at end of file diff --git a/tests/resources/communication/getWithQueryParams.request b/tests/resources/communication/getWithQueryParams.request new file mode 100644 index 0000000..f9a6b32 --- /dev/null +++ b/tests/resources/communication/getWithQueryParams.request @@ -0,0 +1,4 @@ +Outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\)\:\ +\ \ method\:\ \ \ \ \ \ \ \'GET\'\ +\ \ uri\:\ \ \ \ \ \ \ \ \ \ \'\/v1\/get\?source\=EUR\&amount\=1000\&target\=USD\'\ +\ \ headers\:\ \ \ \ \ \ \'X\-WL\-ServerMetaInfo\=\"[^"]*\"\,\ Date\=\"[^"]+\"\,\ Authorization\=\"\*\*\*\*\*\*\*\*\"[^']*\' diff --git a/tests/resources/communication/getWithQueryParams.response b/tests/resources/communication/getWithQueryParams.response new file mode 100644 index 0000000..8f58502 --- /dev/null +++ b/tests/resources/communication/getWithQueryParams.response @@ -0,0 +1,7 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'200\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\ +\ \ \ \"convertedAmount\"\ \:\ 4547504\ +\}\' \ No newline at end of file diff --git a/tests/resources/communication/getWithoutQueryParams.json b/tests/resources/communication/getWithoutQueryParams.json new file mode 100644 index 0000000..d8e50e2 --- /dev/null +++ b/tests/resources/communication/getWithoutQueryParams.json @@ -0,0 +1,3 @@ +{ + "result": "OK" +} \ No newline at end of file diff --git a/tests/resources/communication/getWithoutQueryParams.request b/tests/resources/communication/getWithoutQueryParams.request new file mode 100644 index 0000000..752b670 --- /dev/null +++ b/tests/resources/communication/getWithoutQueryParams.request @@ -0,0 +1,4 @@ +Outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\)\:\ +\ \ method\:\ \ \ \ \ \ \ \'GET\'\ +\ \ uri\:\ \ \ \ \ \ \ \ \ \ \'\/v1\/get\'\ +\ \ headers\:\ \ \ \ \ \ \'X\-WL\-ServerMetaInfo\=\"[^"]*\"\,\ Date\=\"[^"]+\"\,\ Authorization\=\"\*\*\*\*\*\*\*\*\"[^']*\' diff --git a/tests/resources/communication/getWithoutQueryParams.response b/tests/resources/communication/getWithoutQueryParams.response new file mode 100644 index 0000000..ae842f0 --- /dev/null +++ b/tests/resources/communication/getWithoutQueryParams.response @@ -0,0 +1,7 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'200\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\ +\ \ \ \ \"result\"\:\ \"OK\"\ +\}\' \ No newline at end of file diff --git a/tests/resources/communication/notFound.html b/tests/resources/communication/notFound.html new file mode 100644 index 0000000..8537307 --- /dev/null +++ b/tests/resources/communication/notFound.html @@ -0,0 +1 @@ +Not Found \ No newline at end of file diff --git a/tests/resources/communication/notFound.response b/tests/resources/communication/notFound.response new file mode 100644 index 0000000..28e9e39 --- /dev/null +++ b/tests/resources/communication/notFound.response @@ -0,0 +1,5 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'404\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"text\/html\"\,\ Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'text\/html\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'Not\ Found\' \ No newline at end of file diff --git a/tests/resources/communication/postWithBadRequestResponse.json b/tests/resources/communication/postWithBadRequestResponse.json new file mode 100644 index 0000000..f28496c --- /dev/null +++ b/tests/resources/communication/postWithBadRequestResponse.json @@ -0,0 +1,11 @@ +{ + "errorId": "0953f236-9e54-4f23-9556-d66bc757dda8", + "errors": [ + { + "code": "21000020", + "requestId": "24146", + "message": "VALUE **************** OF FIELD CREDITCARDNUMBER DID NOT PASS THE LUHNCHECK", + "httpStatusCode": 400 + } + ] +} \ No newline at end of file diff --git a/tests/resources/communication/postWithBadRequestResponse.request b/tests/resources/communication/postWithBadRequestResponse.request new file mode 100644 index 0000000..f8e27d8 --- /dev/null +++ b/tests/resources/communication/postWithBadRequestResponse.request @@ -0,0 +1,6 @@ +Outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\)\:\ +\ \ method\:\ \ \ \ \ \ \ \'POST\'\ +\ \ uri\:\ \ \ \ \ \ \ \ \ \ \'\/v1\/bad-request\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ X\-WL\-ServerMetaInfo\=\"[^"]*\"\,\ Date\=\"[^"]+\"\,\ Authorization\=\"\*\*\*\*\*\*\*\*\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\;\ charset\=UTF\-8\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\"card\"\:\{\"cardSecurityCode\"\:\"\*\*\*\"\,\"cardNumber\"\:\"\*\*\*\*\*\*\*\*\*\*\*\*3456\"\,\"expiryDate\"\:\"\*\*2024\"\}\}\' diff --git a/tests/resources/communication/postWithBadRequestResponse.response b/tests/resources/communication/postWithBadRequestResponse.response new file mode 100644 index 0000000..d7df12e --- /dev/null +++ b/tests/resources/communication/postWithBadRequestResponse.response @@ -0,0 +1,15 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'400\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\ +\ \ \ \ \"errorId\"\:\ \"0953f236\-9e54\-4f23\-9556\-d66bc757dda8\"\,\ +\ \ \ \ \"errors\"\:\ \[\ +\ \ \ \ \ \ \ \ \{\ +\ \ \ \ \ \ \ \ \ \ \ \ \"code\"\:\ \"21000020\"\,\ +\ \ \ \ \ \ \ \ \ \ \ \ \"requestId\"\:\ \"24146\"\,\ +\ \ \ \ \ \ \ \ \ \ \ \ \"message\"\:\ \"VALUE\ \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\ OF\ FIELD\ CREDITCARDNUMBER\ DID\ NOT\ PASS\ THE\ LUHNCHECK\"\,\ +\ \ \ \ \ \ \ \ \ \ \ \ \"httpStatusCode\"\:\ 400\ +\ \ \ \ \ \ \ \ \}\ +\ \ \ \ \]\ +\}\' \ No newline at end of file diff --git a/tests/resources/communication/postWithCreatedResponse.json b/tests/resources/communication/postWithCreatedResponse.json new file mode 100644 index 0000000..77f8020 --- /dev/null +++ b/tests/resources/communication/postWithCreatedResponse.json @@ -0,0 +1,10 @@ +{ + "creationOutput": { + "additionalReference": "00000012341000059598", + "externalReference": "000000123410000595980000100001" + }, + "payment": { + "id": "000000123410000595980000100001", + "status": "PENDING_APPROVAL" + } +} diff --git a/tests/resources/communication/postWithCreatedResponse.request b/tests/resources/communication/postWithCreatedResponse.request new file mode 100644 index 0000000..b8b4906 --- /dev/null +++ b/tests/resources/communication/postWithCreatedResponse.request @@ -0,0 +1,6 @@ +Outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\)\:\ +\ \ method\:\ \ \ \ \ \ \ \'POST\'\ +\ \ uri\:\ \ \ \ \ \ \ \ \ \ \'\/v1\/created\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ X\-WL\-ServerMetaInfo\=\"[^"]*\"\,\ Date\=\"[^"]+\"\,\ Authorization\=\"\*\*\*\*\*\*\*\*\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\;\ charset\=UTF\-8\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\"card\"\:\{\"cardSecurityCode\"\:\"\*\*\*\"\,\"cardNumber\"\:\"\*\*\*\*\*\*\*\*\*\*\*\*3456\"\,\"expiryDate\"\:\"\*\*2024\"\}\}\' diff --git a/tests/resources/communication/postWithCreatedResponse.response b/tests/resources/communication/postWithCreatedResponse.response new file mode 100644 index 0000000..ab2e257 --- /dev/null +++ b/tests/resources/communication/postWithCreatedResponse.response @@ -0,0 +1,10 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'201\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ Dummy\=\"\"\,\ Location\=\"http\:\/\/localhost\/v1\/created\/000000123410000595980000100001\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\ +\ \ \ \ \"payment\"\:\ \{\ +\ \ \ \ \ \ \ \ \"id\"\:\ \"000000123410000595980000100001\"\,\ +\ \ \ \ \ \ \ \ \"status\"\:\ \"PENDING\_APPROVAL\"\ +\ \ \ \ \}\ +\}\' diff --git a/tests/resources/communication/unknownServerError.json b/tests/resources/communication/unknownServerError.json new file mode 100644 index 0000000..270dc00 --- /dev/null +++ b/tests/resources/communication/unknownServerError.json @@ -0,0 +1,10 @@ +{ + "errorId": "fbff1179-7ba4-4894-9021-d8a0011d23a7", + "errors": [ + { + "code": "9999", + "message": "UNKNOWN_SERVER_ERROR", + "httpStatusCode": 500 + } + ] +} \ No newline at end of file diff --git a/tests/resources/communication/unknownServerError.response b/tests/resources/communication/unknownServerError.response new file mode 100644 index 0000000..f128453 --- /dev/null +++ b/tests/resources/communication/unknownServerError.response @@ -0,0 +1,14 @@ +Incoming\ response\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ ms\)\:\ +\ \ status\-code\:\ \ \'500\'\ +\ \ headers\:\ \ \ \ \ \ \'Content\-Type\=\"application\/json\"\,\ Dummy\=\"\"\,\ Date\=\"[^"]+\"[^']*\'\ +\ \ content\-type\:\ \'application\/json\'\ +\ \ body\:\ \ \ \ \ \ \ \ \ \'\{\ +\ \ \ \ \"errorId\"\:\ \"fbff1179\-7ba4\-4894\-9021\-d8a0011d23a7\"\,\ +\ \ \ \ \"errors\"\:\ \[\ +\ \ \ \ \ \ \ \ \{\ +\ \ \ \ \ \ \ \ \ \ \ \ \"code\"\:\ \"9999\"\,\ +\ \ \ \ \ \ \ \ \ \ \ \ \"message\"\:\ \"UNKNOWN\_SERVER\_ERROR\"\,\ +\ \ \ \ \ \ \ \ \ \ \ \ \"httpStatusCode\"\:\ 500\ +\ \ \ \ \ \ \ \ \}\ +\ \ \ \ \]\ +\}\' \ No newline at end of file diff --git a/tests/resources/configuration.oauth2.ini b/tests/resources/configuration.oauth2.ini new file mode 100644 index 0000000..dcfa035 --- /dev/null +++ b/tests/resources/configuration.oauth2.ini @@ -0,0 +1,9 @@ +# Worldline Acquiring platform connection settings +[AcquiringSDK] +acquiring.api.endpoint.host=api.preprod.acquiring.worldline-solutions.com +acquiring.api.authorizationType=OAuth2 +acquiring.api.oauth2.tokenUri=https://sso.preprod.acquiring.worldline-solutions.com/auth/realms/acquiring_api/protocol/openid-acquiring/token +acquiring.api.connectTimeout=1000 +acquiring.api.socketTimeout=1000 +acquiring.api.maxConnections=100 +acquiring.api.integrator=Worldline diff --git a/tests/resources/configuration.proxy.ini b/tests/resources/configuration.proxy.ini new file mode 100644 index 0000000..cf1b7e2 --- /dev/null +++ b/tests/resources/configuration.proxy.ini @@ -0,0 +1,14 @@ +# Worldline Acquiring platform proxy connection settings +[AcquiringSDK] +acquiring.api.endpoint.host=api.preprod.acquiring.worldline-solutions.com +acquiring.api.authorizationType=OAuth2 +acquiring.api.oauth2.tokenUri=https://sso.preprod.acquiring.worldline-solutions.com/auth/realms/acquiring_api/protocol/openid-acquiring/token +acquiring.api.connectTimeout=1000 +acquiring.api.socketTimeout=1000 +acquiring.api.integrator=Worldline +# change uri to preferred proxy +acquiring.api.proxy.uri=https://proxy.example.local:8888 +# change username and password to authentication credentials for the proxy, omit the entries if no authentication is required +# authentication is done using basic proxy authentication. Therefore, to avoid leaking credentials you should connect to the proxy using SSL +acquiring.api.proxy.username=test-user +acquiring.api.proxy.password=test-password diff --git a/tests/resources/log/bodyNoObfuscation.json b/tests/resources/log/bodyNoObfuscation.json new file mode 100644 index 0000000..2d4920a --- /dev/null +++ b/tests/resources/log/bodyNoObfuscation.json @@ -0,0 +1,7 @@ +{ + "amount": { + "currencyCode": "EUR", + "amount": 1000 + }, + "authorizationType": "PRE_AUTHORIZATION" +} diff --git a/tests/resources/log/bodyWithBinObfuscated.json b/tests/resources/log/bodyWithBinObfuscated.json new file mode 100644 index 0000000..e27318f --- /dev/null +++ b/tests/resources/log/bodyWithBinObfuscated.json @@ -0,0 +1,3 @@ +{ + "bin": "123456**" +} \ No newline at end of file diff --git a/tests/resources/log/bodyWithBinOriginal.json b/tests/resources/log/bodyWithBinOriginal.json new file mode 100644 index 0000000..967ec1b --- /dev/null +++ b/tests/resources/log/bodyWithBinOriginal.json @@ -0,0 +1,3 @@ +{ + "bin": "12345678" +} \ No newline at end of file diff --git a/tests/resources/log/bodyWithCardCustomObfuscated.json b/tests/resources/log/bodyWithCardCustomObfuscated.json new file mode 100644 index 0000000..3d0a9f6 --- /dev/null +++ b/tests/resources/log/bodyWithCardCustomObfuscated.json @@ -0,0 +1,13 @@ +{ + "amount": { + "currencyCode": "CAD", + "amount": 2345 + }, + "cardPaymentData": { + "cardData": { + "cardSecurityCode": "***", + "cardNumber": "123456******3456", + "expiryDate": "**2024" + } + } +} diff --git a/tests/resources/log/bodyWithCardObfuscated.json b/tests/resources/log/bodyWithCardObfuscated.json new file mode 100644 index 0000000..07c0e73 --- /dev/null +++ b/tests/resources/log/bodyWithCardObfuscated.json @@ -0,0 +1,13 @@ +{ + "amount": { + "currencyCode": "CAD", + "amount": 2345 + }, + "cardPaymentData": { + "cardData": { + "cardSecurityCode": "***", + "cardNumber": "************3456", + "expiryDate": "**2024" + } + } +} diff --git a/tests/resources/log/bodyWithCardOriginal.json b/tests/resources/log/bodyWithCardOriginal.json new file mode 100644 index 0000000..7910cf3 --- /dev/null +++ b/tests/resources/log/bodyWithCardOriginal.json @@ -0,0 +1,13 @@ +{ + "amount": { + "currencyCode": "CAD", + "amount": 2345 + }, + "cardPaymentData": { + "cardData": { + "cardSecurityCode": "123", + "cardNumber": "1234567890123456", + "expiryDate": "122024" + } + } +} diff --git a/tests/resources/log/bodyWithObjectObfuscated.json b/tests/resources/log/bodyWithObjectObfuscated.json new file mode 100644 index 0000000..960b96a --- /dev/null +++ b/tests/resources/log/bodyWithObjectObfuscated.json @@ -0,0 +1,5 @@ +{ + "name": ****, + "name": { + } +} diff --git a/tests/resources/log/bodyWithObjectOriginal.json b/tests/resources/log/bodyWithObjectOriginal.json new file mode 100644 index 0000000..7a44a50 --- /dev/null +++ b/tests/resources/log/bodyWithObjectOriginal.json @@ -0,0 +1,5 @@ +{ + "name": true, + "name": { + } +} diff --git a/tests/run_all_tests.py b/tests/run_all_tests.py new file mode 100644 index 0000000..696d784 --- /dev/null +++ b/tests/run_all_tests.py @@ -0,0 +1,24 @@ +#!python +import unittest +import sys +import os +# append to pythonpath to make imports work +sys.path.insert(0, os.path.abspath("..")) + + +def load_tests(loader, tests, pattern): + """ Discover and load all tests in all files named ``test_*.py`` in ``tests`` + + Overrides default test loading behavior to load all tests in subfolders + """ + unit_dir = os.path.join(os.path.dirname(__file__), "unit") + unit_tests = loader.discover(start_dir=unit_dir, pattern="test_*.py", top_level_dir=unit_dir) + integration_dir = os.path.join(os.path.dirname(__file__), "integration") + integration_tests = loader.discover(start_dir=integration_dir, pattern="test_*.py", top_level_dir=integration_dir) + tests.addTests(unit_tests) + tests.addTests(integration_tests) + return tests + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/run_integration_tests.py b/tests/run_integration_tests.py new file mode 100644 index 0000000..595ad29 --- /dev/null +++ b/tests/run_integration_tests.py @@ -0,0 +1,21 @@ +#!python +import unittest +import sys +import os +# append to pythonpath to make imports work +sys.path.insert(0, os.path.abspath("..")) + + +def load_tests(loader, tests, pattern): + """ Discover and load all integration tests in all files named ``test_*.py`` in ``../integration/`` + + Overrides default test loading behavior to load only the tests in the integration subfolder + """ + integration_dir = os.path.join(os.path.dirname(__file__), "integration") + integration_tests = loader.discover(start_dir=integration_dir, pattern="test_*.py") + tests.addTests(integration_tests) + return tests + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/run_unit_tests.py b/tests/run_unit_tests.py new file mode 100644 index 0000000..cd6009d --- /dev/null +++ b/tests/run_unit_tests.py @@ -0,0 +1,21 @@ +#!python +import unittest +import sys +import os +# append to pythonpath to make imports work +sys.path.insert(0, os.path.abspath("..")) + + +def load_tests(loader, tests, pattern): + """ Discover and load all unit tests in all files named ``test_*.py`` in ``../unit/`` + + Overrides default test loading behavior to only load tests in the unit folder + """ + unit_dir = os.path.join(os.path.dirname(__file__), "unit") + unit_tests = loader.discover(start_dir=unit_dir, pattern="test_*.py") + tests.addTests(unit_tests) + return tests + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/comparable_param.py b/tests/unit/comparable_param.py new file mode 100644 index 0000000..10f9ede --- /dev/null +++ b/tests/unit/comparable_param.py @@ -0,0 +1,28 @@ +from worldline.acquiring.sdk.communication.request_param import RequestParam + + +class ComparableParam(RequestParam): + """Request param object that can be compared to request param objects for attribute equality""" + + def __init__(self, name, value): + super(ComparableParam, self).__init__(name, value) + + def __eq__(self, other): + for attribute in vars(self): + if not hasattr(other, attribute): + return False + else: + s_attr = getattr(self, attribute) + o_attr = getattr(other, attribute) + if s_attr != o_attr: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + _hash = "" + for attribute in vars(self): + _hash += getattr(self, attribute) + return _hash diff --git a/tests/unit/mock_authenticator.py b/tests/unit/mock_authenticator.py new file mode 100644 index 0000000..bd24221 --- /dev/null +++ b/tests/unit/mock_authenticator.py @@ -0,0 +1,11 @@ +from typing import Optional, Sequence +from urllib.parse import ParseResult + +from worldline.acquiring.sdk.authentication.authenticator import Authenticator +from worldline.acquiring.sdk.communication.request_header import RequestHeader + + +class MockAuthenticator(Authenticator): + def get_authorization(self, http_method: str, resource_uri: ParseResult, + request_headers: Optional[Sequence[RequestHeader]]) -> str: + return 'Test' diff --git a/tests/unit/server_mock_utils.py b/tests/unit/server_mock_utils.py new file mode 100644 index 0000000..19af59a --- /dev/null +++ b/tests/unit/server_mock_utils.py @@ -0,0 +1,71 @@ +import socketserver +import contextlib +import _thread +from http.server import BaseHTTPRequestHandler + +import time + +from .mock_authenticator import MockAuthenticator + +from worldline.acquiring.sdk.communicator import Communicator +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + + +def create_handler(call_able): + """Creates a handler that serves requests by calling the callable object + with this handler as argument + """ + class RequestHandler(BaseHTTPRequestHandler): + + def do_GET(self): + call_able(self) + time.sleep(0.1) # sleep to avoid dropping the client before it can read the response + + def do_POST(self): + call_able(self) + time.sleep(0.1) # sleep to avoid dropping the client before it can read the response + + def do_HEAD(self): + pass + + def do_DELETE(self): + call_able(self) + time.sleep(0.1) # sleep to avoid dropping the client before it can read the response + return RequestHandler + + +@contextlib.contextmanager +def create_server_listening(call_able): + """Context manager that creates a thread with a server at localhost which listens for requests + and responds by calling the *call_able* function. + + :param call_able: a callable function to handle incoming requests, when a request comes in + the function will be called with a SimpleHTTPRequestHandler to handle the request + :return the url where the server is listening (http://localhost:port) + """ + server = socketserver.TCPServer(('localhost', 0), create_handler(call_able), bind_and_activate=True) + try: + # frequent polling server for a faster server shutdown and faster tests + _thread.start_new(server.serve_forever, (0.1,)) + yield 'http://localhost:'+str(server.server_address[1]) + finally: + server.shutdown() + server.server_close() + + +def create_communicator(httphost, connect_timeout=0.500, socket_timeout=0.500, + max_connections=CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS): + connection = DefaultConnection(connect_timeout, socket_timeout, max_connections) + authenticator = MockAuthenticator() + metadata_provider = MetadataProvider("Worldline") + return Communicator(httphost, connection, authenticator, metadata_provider, DefaultMarshaller.instance()) + + +def create_client(httphost, connect_timeout=0.500, socket_timeout=0.500, + max_connections=CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS): + communicator = create_communicator(httphost, connect_timeout, socket_timeout, max_connections) + return Factory.create_client_from_communicator(communicator) diff --git a/tests/unit/test_body_obfuscator.py b/tests/unit/test_body_obfuscator.py new file mode 100644 index 0000000..76e4681 --- /dev/null +++ b/tests/unit/test_body_obfuscator.py @@ -0,0 +1,87 @@ +import os +import unittest + +from tests import file_utils + +from worldline.acquiring.sdk.log.body_obfuscator import BodyObfuscator + + +class BodyObfuscatorTest(unittest.TestCase): + """Tests if the body obfuscator is capable of obfuscating bodies of requests""" + + def test_obfuscate_body_none_as_body(self): + """Test that the obfuscate_body function can deal with a body = none and produce a result of none""" + body = None + + obfuscated_body = BodyObfuscator.default_body_obfuscator().obfuscate_body(body) + self.assertIsNone(obfuscated_body) + + def test_obfuscate_body_empty(self): + """Tests if the obfuscate_body function is capable of obfuscating an empty body""" + body = "" + + obfuscated_body = BodyObfuscator.default_body_obfuscator().obfuscate_body(body) + self.assertEqual("", obfuscated_body) + + def test_obfuscate_body_card(self): + """Tests that the obfuscate_body function correctly obfuscates a json containing payment card card data""" + self.obfuscate_body_match("bodyWithCardOriginal.json", + "bodyWithCardObfuscated.json") + + def test_obfuscate_body_card_custom_rule(self): + """Tests that the obfuscate_body function correctly obfuscates a json containing payment card card data with a custom rule""" + def obfuscate_custom(value): + start = 6 + end = len(value) - 4 + value_between = '*' * (end - start) + return value[:start] + value_between + value[end:] + + body_obfuscator = BodyObfuscator(additional_rules={ + "card": obfuscate_custom + }) + self.obfuscate_body_match("bodyWithCardOriginal.json", + "bodyWithCardObfuscated.json", + body_obfuscator=body_obfuscator) + + def test_obfuscate_body_bin(self): + """Tests that the obfuscate_body function correctly obfuscates a json containing bin data""" + self.obfuscate_body_match("bodyWithBinOriginal.json", + "bodyWithBinObfuscated.json") + + def test_obfuscate_body_nothing(self): + """Tests that the obfuscate_body function does not touch data that does not need to be obfuscated""" + self.obfuscate_body_no_match("bodyNoObfuscation.json") + + def test_obfuscate_body_object(self): + """Tests that the obfuscate_body function correctly obfuscates a json containing an object that does not need to be obfuscated""" + self.obfuscate_body_match("bodyWithObjectOriginal.json", + "bodyWithObjectObfuscated.json") + + def obfuscate_body_match(self, original_resource, obfuscated_resource, body_obfuscator=BodyObfuscator.default_body_obfuscator()): + """Tests that the obfuscate_body function obfuscates the json in original_resource to the json in obfuscated_resource + + original_resource is the path to a json file that contains one or more data entries to be obfuscated + obfuscated_resource is the path to a json file containing the expected obfuscated json result of the obfuscation + """ + body = _read_resource(original_resource) + expected = _read_resource(obfuscated_resource) + + obfuscated_body = body_obfuscator.obfuscate_body(body) + + self.assertEqual(expected, obfuscated_body) + + def obfuscate_body_no_match(self, resource): + """Tests that the obfuscate_body function does not obfuscate the json given in the resource file + + resource is the path to a json file that contains no data that should be obfuscated + """ + self.obfuscate_body_match(resource, resource) + + +# reads a file names file_name stored under resources/log +def _read_resource(file_name): return file_utils.read_file( + os.path.join("log", file_name)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 0000000..17b27e5 --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,83 @@ +import unittest + +from datetime import timedelta +from unittest.mock import Mock, MagicMock + +from tests.unit.test_factory import PROPERTIES_URI_OAUTH2, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET + +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.communication.connection import Connection +from worldline.acquiring.sdk.communication.pooled_connection import PooledConnection + + +class ClientTest(unittest.TestCase): + """Tests for the Client class testing if connection settings are propagated properly to the connection object + """ + + def test_close_idle_connection_not_pooled(self): + """Tests that the setting to close an idle connection in a client propagates to the connection + for an unpooled connection + """ + mock = MagicMock(spec=Connection, autospec=True) + function_mock = Mock(name="close_idle_connections_mock") + mock.attach_mock(function_mock, "close_idle_connections") + communicator = Factory.create_communicator_from_file(configuration_file_name=PROPERTIES_URI_OAUTH2, + authorization_id=OAUTH2_CLIENT_ID, authorization_secret=OAUTH2_CLIENT_SECRET, + connection=mock) + client = Factory.create_client_from_communicator(communicator) + + client.close_idle_connections(timedelta(seconds=5)) # seconds + + function_mock.assert_not_called() + + def test_close_idle_connection_pooled(self): + """Tests that the setting to close an idle connection in a client propagates to the connection + for a pooled connection + """ + pooled_mock = MagicMock(spec=PooledConnection, autospec=True) + function_mock = Mock(name="close_idle_connections_mock") + pooled_mock.attach_mock(function_mock, "close_idle_connections") + communicator = Factory.create_communicator_from_file(configuration_file_name=PROPERTIES_URI_OAUTH2, + authorization_id=OAUTH2_CLIENT_ID, authorization_secret=OAUTH2_CLIENT_SECRET, + connection=pooled_mock) + client = Factory.create_client_from_communicator(communicator) + + client.close_idle_connections(timedelta(seconds=5)) # seconds + + function_mock.assert_called_once_with(timedelta(seconds=5)) + + def test_close_expired_connections_not_pooled(self): + """Tests that the setting to close an expired connection in a client does not propagate to the connection + for an unpooled connection + """ + mock = MagicMock(spec=Connection, autospec=True) + function_mock = Mock(name="close_expired_connections_mock") + mock.attach_mock(function_mock, "close_expired_connections") + communicator = Factory.create_communicator_from_file(configuration_file_name=PROPERTIES_URI_OAUTH2, + authorization_id=OAUTH2_CLIENT_ID, authorization_secret=OAUTH2_CLIENT_SECRET, + connection=mock) + client = Factory.create_client_from_communicator(communicator) + + client.close_expired_connections() + + function_mock.assert_not_called() + + def test_close_expired_connections_pooled(self): + """Tests that the setting to close an expired connection in a client propagates to the connection + for a pooled connection + """ + pooled_mock = MagicMock(spec=PooledConnection, autospec=True) + function_mock = Mock(name="close_expired_connections_mock") + pooled_mock.attach_mock(function_mock, "close_expired_connections") + communicator = Factory.create_communicator_from_file(configuration_file_name=PROPERTIES_URI_OAUTH2, + authorization_id=OAUTH2_CLIENT_ID, authorization_secret=OAUTH2_CLIENT_SECRET, + connection=pooled_mock) + client = Factory.create_client_from_communicator(communicator) + + client.close_expired_connections() + + function_mock.assert_called_once_with() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_communicator.py b/tests/unit/test_communicator.py new file mode 100644 index 0000000..554bc23 --- /dev/null +++ b/tests/unit/test_communicator.py @@ -0,0 +1,56 @@ +# -*-coding: UTF-8 -*- +import unittest + +from urllib.parse import urlparse +from unittest.mock import MagicMock + +from worldline.acquiring.sdk.communicator import Communicator +from worldline.acquiring.sdk.authentication.authenticator import Authenticator +from worldline.acquiring.sdk.communication.connection import Connection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.communication.request_param import RequestParam +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + + +class CommunicatorTest(unittest.TestCase): + """Contains tests that test if the communicator can construct proper urls + if given the base url, a relative url and possibly a list of request parameters + """ + + def test_to_uri_without_request_parameters(self): + """Tests if the communicator can correctly construct an url using a known base url and a relative url""" + communicator = Communicator(api_endpoint=urlparse("https://api.preprod.acquiring.worldline-solutions.com"), + connection=MagicMock(spec=Connection, autospec=True), + authenticator=MagicMock(spec=Authenticator, autospec=True), + metadata_provider=MetadataProvider("Worldline"), + marshaller=DefaultMarshaller.instance()) + + uri1 = communicator._to_absolute_uri("services/v1/100812/520000214/dcc-rates", []) + uri2 = communicator._to_absolute_uri("/services/v1/100812/520000214/dcc-rates", []) + + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com/services/v1/100812/520000214/dcc-rates", uri1.geturl()) + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com/services/v1/100812/520000214/dcc-rates", uri2.geturl()) + + def test_to_uri_with_request_parameters(self): + """Tests if the communicator can correctly construct an url + using a known base url, a relative url and a list of request parameters + """ + requestparams = [RequestParam("amount", "123"), RequestParam("source", "USD"), + RequestParam("target", "EUR"), RequestParam("dummy", "é&%=")] + communicator = Communicator(api_endpoint=urlparse("https://api.preprod.acquiring.worldline-solutions.com"), + connection=MagicMock(spec=Connection, autospec=True), + authenticator=MagicMock(spec=Authenticator, autospec=True), + metadata_provider=MetadataProvider("Worldline"), + marshaller=DefaultMarshaller.instance()) + + uri1 = communicator._to_absolute_uri("services/v1/100812/520000214/dcc-rates", requestparams) + uri2 = communicator._to_absolute_uri("/services/v1/100812/520000214/dcc-rates", requestparams) + + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com/services/v1/100812/520000214/dcc-rates" + "?amount=123&source=USD&target=EUR&dummy=%C3%A9%26%25%3D", uri1.geturl()) + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com/services/v1/100812/520000214/dcc-rates" + "?amount=123&source=USD&target=EUR&dummy=%C3%A9%26%25%3D", uri2.geturl()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_communicator_configuration.py b/tests/unit/test_communicator_configuration.py new file mode 100644 index 0000000..3c8e4ad --- /dev/null +++ b/tests/unit/test_communicator_configuration.py @@ -0,0 +1,153 @@ +import configparser +import unittest + +from worldline.acquiring.sdk.authentication.authorization_type import AuthorizationType +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration + + +class CommunicatorConfigurationTest(unittest.TestCase): + """Contains tests testing that the correct communicator configuration can be made from a properties file""" + + def setUp(self): + """Initialize a set of commonly used configurations""" + self.config = configparser.ConfigParser() + + self.config.add_section("AcquiringSDK") + self.config.set('AcquiringSDK', 'acquiring.api.endpoint.host', "api.preprod.acquiring.worldline-solutions.com") + self.config.set('AcquiringSDK', 'acquiring.api.authorizationType', 'OAuth2') + self.config.set('AcquiringSDK', 'acquiring.api.oauth2.tokenUri', 'https://sso.preprod.acquiring.worldline-solutions.com/auth/realms/acquiring_api/protocol/openid-acquiring/token') + self.config.set('AcquiringSDK', 'acquiring.api.connectTimeout', '20') + self.config.set('AcquiringSDK', 'acquiring.api.socketTimeout', '10') + + def tearDown(self): + self.config = None + + def assertDefaults(self, communicator_config): + """Tests commonly used settings for testing url, authorization type, timeouts and max_connections""" + # this argument should not be needed VV + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com", communicator_config.api_endpoint.geturl()) + self.assertEqual(AuthorizationType.get_authorization("OAuth2"), communicator_config.authorization_type) + self.assertEqual(20, communicator_config.connect_timeout) + self.assertEqual(10, communicator_config.socket_timeout) + + self.assertEqual(CommunicatorConfiguration().DEFAULT_MAX_CONNECTIONS, communicator_config.max_connections) + + def test_construct_from_properties_without_proxy(self): + """Test if a CommunicatorConfiguration can be constructed correctly from a list of properties""" + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertDefaults(communicator_config) + self.assertIsNone(communicator_config.authorization_id) + self.assertIsNone(communicator_config.authorization_secret) + self.assertIsNone(communicator_config.proxy_configuration) + self.assertIsNone(communicator_config.integrator) + self.assertIsNone(communicator_config.shopping_cart_extension) + + def test_construct_from_properties_with_proxy_without_authentication(self): + """Tests if a CommunicatorConfiguration can be constructed correctly from settings including a proxy""" + self.config.set('AcquiringSDK', "acquiring.api.proxy.uri", "http://proxy.example.org:3128") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertDefaults(communicator_config) + self.assertIsNone(communicator_config.authorization_id) + self.assertIsNone(communicator_config.authorization_secret) + proxy_config = communicator_config.proxy_configuration + self.assertIsNotNone(proxy_config) + self.assertEqual("http", proxy_config.scheme) + self.assertEqual("proxy.example.org", proxy_config.host) + self.assertEqual(3128, proxy_config.port) + self.assertIsNone(proxy_config.username) + self.assertIsNone(proxy_config.password) + + def test_construct_from_properties_with_proxy_authentication(self): + """Tests if a CommunicatorConfiguration can be constructed correctly + from settings with a proxy and authentication + """ + self.config.set('AcquiringSDK', "acquiring.api.proxy.uri", "http://proxy.example.org:3128") + self.config.set('AcquiringSDK', "acquiring.api.proxy.username", "proxy-username") + self.config.set('AcquiringSDK', "acquiring.api.proxy.password", "proxy-password") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertDefaults(communicator_config) + self.assertIsNone(communicator_config.authorization_id) + self.assertIsNone(communicator_config.authorization_secret) + proxy_config = communicator_config.proxy_configuration + self.assertIsNotNone(proxy_config) + self.assertEqual("http", proxy_config.scheme) + self.assertEqual("proxy.example.org", proxy_config.host) + self.assertEqual(3128, proxy_config.port) + self.assertEqual("proxy-username", proxy_config.username) + self.assertEqual("proxy-password", proxy_config.password) + + def test_construct_from_properties_with_max_connection(self): + """Tests if a CommunicatorConfiguration can be constructed correctly + from settings that contain a different number of maximum connections + """ + self.config.set("AcquiringSDK", "acquiring.api.maxConnections", "100") + + communicator_config = CommunicatorConfiguration(self.config) + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com", communicator_config.api_endpoint.geturl()) + self.assertEqual(AuthorizationType.get_authorization("OAuth2"), communicator_config.authorization_type) + self.assertEqual(20, communicator_config.connect_timeout) + self.assertEqual(10, communicator_config.socket_timeout) + self.assertEqual(100, communicator_config.max_connections) + self.assertIsNone(communicator_config.authorization_id) + self.assertIsNone(communicator_config.authorization_secret) + self.assertIsNone(communicator_config.proxy_configuration) + + def test_construct_from_properties_with_host_and_scheme(self): + """Tests that constructing a communicator configuration from a host and port correctly processes this info""" + self.config.set("AcquiringSDK", "acquiring.api.endpoint.scheme", "http") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertEqual("http://api.preprod.acquiring.worldline-solutions.com", communicator_config.api_endpoint.geturl()) + + def test_construct_from_properties_with_host_and_port(self): + """Tests that constructing a communicator configuration from a host and port correctly processes this info""" + + self.config.set("AcquiringSDK", "acquiring.api.endpoint.port", "8443") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertEqual("https://api.preprod.acquiring.worldline-solutions.com:8443", communicator_config.api_endpoint.geturl()) + + def test_construct_from_properties_with_host_scheme_port(self): + """Tests that constructing a communicator configuration from host, scheme and port correctly processes this info + """ + self.config.set("AcquiringSDK", "acquiring.api.endpoint.scheme", "http") + self.config.set("AcquiringSDK", "acquiring.api.endpoint.port", "8080") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertEqual("http://api.preprod.acquiring.worldline-solutions.com:8080", communicator_config.api_endpoint.geturl()) + + def test_construct_from_properties_with_metadata(self): + """Tests that constructing a communicator configuration + using integrator and shopping cart data constructs properly + """ + self.config.set("AcquiringSDK", "acquiring.api.integrator", "Worldline.Integrator") + self.config.set("AcquiringSDK", "acquiring.api.shoppingCartExtension.creator", "Worldline.Creator") + self.config.set("AcquiringSDK", "acquiring.api.shoppingCartExtension.name", "Worldline.ShoppingCarts") + self.config.set("AcquiringSDK", "acquiring.api.shoppingCartExtension.version", "1.0") + self.config.set("AcquiringSDK", "acquiring.api.shoppingCartExtension.extensionId", "ExtensionId") + + communicator_config = CommunicatorConfiguration(self.config) + + self.assertDefaults(communicator_config) + self.assertIsNone(communicator_config.authorization_id) + self.assertIsNone(communicator_config.authorization_secret) + self.assertIsNone(communicator_config.proxy_configuration) + self.assertEqual("Worldline.Integrator", communicator_config.integrator) + self.assertIsNotNone(communicator_config.shopping_cart_extension) + self.assertEqual("Worldline.Creator", communicator_config.shopping_cart_extension.creator) + self.assertEqual("Worldline.ShoppingCarts", communicator_config.shopping_cart_extension.name) + self.assertEqual("1.0", communicator_config.shopping_cart_extension.version) + self.assertEqual("ExtensionId", communicator_config.shopping_cart_extension.extension_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_data_object.py b/tests/unit/test_data_object.py new file mode 100644 index 0000000..b8dc4fd --- /dev/null +++ b/tests/unit/test_data_object.py @@ -0,0 +1,74 @@ +import unittest +from datetime import date, datetime, timedelta, timezone + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class DateObjectTest(unittest.TestCase): + + def test_parse_date(self): + params = [('2023-09-20', date(2023, 9, 20)), ('2024-02-29', date(2024, 2, 29))] + for s, expected in params: + self.assertEqual(expected, DataObject.parse_date(s)) + + def test_format_date(self): + params = [(date(2023, 9, 20), '2023-09-20'), (date(2024, 2, 29), '2024-02-29')] + for d, expected in params: + self.assertEqual(expected, DataObject.format_date(d)) + + def test_format_and_parse_date(self): + params = [date(2023, 9, 20), date(2024, 2, 29)] + for d in params: + s = DataObject.format_date(d) + self.assertEqual(d, DataObject.parse_date(s)) + + def test_parse_datetime(self): + params = [ + ('2023-10-10T08:00+02:00', datetime(2023, 10, 10, 8, 0, tzinfo=timezone(timedelta(hours=2)))), + ('2023-10-10T08:00Z', datetime(2023, 10, 10, 8, 0, tzinfo=timezone.utc)), + ('2020-01-01T12:00:00Z', datetime(2020, 1, 1, 12, 0, second=0, tzinfo=timezone.utc)), + ('2023-10-10T08:00:01.234+02:00', datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone(timedelta(hours=2)))), + ('2023-10-10T08:00:01.234Z', datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone.utc)), + ('2020-01-01T12:00:00.123456Z', datetime(2020, 1, 1, 12, 0, second=0, microsecond=123456, tzinfo=timezone.utc)), + ] + for s, expected in params: + self.assertEqual(expected, DataObject.parse_datetime(s)) + + def test_format_datetime(self): + params = [ + (datetime(2023, 10, 10, 8, 0, tzinfo=timezone(timedelta(hours=2))), '2023-10-10T08:00:00.000+02:00'), + (datetime(2023, 10, 10, 8, 0, tzinfo=timezone.utc), '2023-10-10T08:00:00.000+00:00'), + (datetime(2020, 1, 1, 12, 0, second=0, tzinfo=timezone.utc), '2020-01-01T12:00:00.000+00:00'), + (datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone(timedelta(hours=2))), '2023-10-10T08:00:01.234+02:00'), + (datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone.utc), '2023-10-10T08:00:01.234+00:00'), + (datetime(2020, 1, 1, 12, 0, second=0, microsecond=123456, tzinfo=timezone.utc), '2020-01-01T12:00:00.123+00:00'), + ] + for dt, expected in params: + self.assertEqual(expected, DataObject.format_datetime(dt)) + + def test_format_and_parse_datetime(self): + params = [ + datetime(2023, 10, 10, 8, 0, tzinfo=timezone(timedelta(hours=2))), + datetime(2023, 10, 10, 8, 0, tzinfo=timezone.utc), + datetime(2020, 1, 1, 12, 0, second=0, tzinfo=timezone.utc), + datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone(timedelta(hours=2))), + datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000, tzinfo=timezone.utc), + datetime(2020, 1, 1, 12, 0, second=0, microsecond=123456, tzinfo=timezone.utc), + ] + for dt in params: + s = DataObject.format_datetime(dt) + self.assertEqual(int(dt.timestamp() * 1000), int(DataObject.parse_datetime(s).timestamp() * 1000)) + + def test_format_and_parse_datetime_no_timezone(self): + params = [ + datetime(2023, 10, 10, 8, 0), + datetime(2020, 1, 1, 12, 0, second=0), + datetime(2023, 10, 10, 8, 0, second=1, microsecond=234000), + ] + for dt in params: + s = DataObject.format_datetime(dt) + self.assertEqual(dt.timestamp(), DataObject.parse_datetime(s).timestamp()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_default_connection.py b/tests/unit/test_default_connection.py new file mode 100644 index 0000000..cd671e6 --- /dev/null +++ b/tests/unit/test_default_connection.py @@ -0,0 +1,152 @@ +import unittest +import warnings + +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.proxy_configuration import ProxyConfiguration +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.log.response_log_message import ResponseLogMessage +from worldline.acquiring.sdk.log.sys_out_communicator_logger import SysOutCommunicatorLogger + +CONNECT_TIMEOUT = 10 +SOCKET_TIMEOUT = 20 +MAX_CONNECTIONS = 100 + + +# noinspection PyTypeChecker +class DefaultConnectionTest(unittest.TestCase): + """Tests that a DefaultConnection can be constructed with a multitude of settings""" + + def test_log_unicode_2(self): + """Tests if requests can be logged correctly""" + logger = SysOutCommunicatorLogger() + message = ResponseLogMessage(request_id="aaa", + status_code=2345, + duration=45.32) + body = u"Schr\xf6der" + content = "JSON" + message.set_body(body, content) + logger.log_response(message) + + def test_log_unicode(self): + """Tests if requests can be logged correctly""" + logger = SysOutCommunicatorLogger() + message = ResponseLogMessage(request_id="aaa", + status_code=2345, + duration=45.32) + body = u"Schr\u0e23\u0e16der" + content = "JSON" + message.set_body(body, content) + logger.log_response(message) + + def test_construct_without_proxy(self): + """Tests construction of a DefaultConnection without using a proxy""" + connection = DefaultConnection(CONNECT_TIMEOUT, SOCKET_TIMEOUT) + + self.assertTimeouts(self, connection, CONNECT_TIMEOUT, SOCKET_TIMEOUT) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertMaxConnections(self, connection, CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS, None) + self.assertNoProxy(self, connection) + + def test_construct_with_proxy_without_authentication(self): + """Tests construction of a DefaultConnection with an unauthenticated proxy""" + proxy_config = ProxyConfiguration.from_uri("http://test-proxy") + + connection = DefaultConnection(CONNECT_TIMEOUT, SOCKET_TIMEOUT, proxy_configuration=proxy_config) + + self.assertTimeouts(self, connection, CONNECT_TIMEOUT, SOCKET_TIMEOUT) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertMaxConnections(self, connection, + CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS, proxy_config) + self.assertProxy(self, connection, proxy_config) + + def test_construct_with_proxy_with_authentication(self): + """Tests construction of a DefaultConnection with an authenticated proxy""" + proxy_config = ProxyConfiguration.from_uri("http://test-proxy", "test-username", "test-password") + + connection = DefaultConnection(CONNECT_TIMEOUT, SOCKET_TIMEOUT, proxy_configuration=proxy_config) + + self.assertTimeouts(self, connection, CONNECT_TIMEOUT, SOCKET_TIMEOUT) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertMaxConnections(self, connection, + CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS, proxy_config) + self.assertProxy(self, connection, proxy_config) + + def test_construct_with_max_connections_without_proxy(self): + """Tests construction of a DefaultConnection with a different amount of max connections and no proxy""" + connection = DefaultConnection(CONNECT_TIMEOUT, SOCKET_TIMEOUT, MAX_CONNECTIONS) + + self.assertTimeouts(self, connection, CONNECT_TIMEOUT, SOCKET_TIMEOUT) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertMaxConnections(self, connection, MAX_CONNECTIONS, None) + self.assertNoProxy(self, connection) + + def test_construct_with_max_connections_with_proxy(self): + """Tests construction of a DefaultConnection + with a different amount of max connections and an unauthenticated proxy + """ + proxy_config = ProxyConfiguration.from_uri("http://test-proxy") + + connection = DefaultConnection(CONNECT_TIMEOUT, SOCKET_TIMEOUT, + MAX_CONNECTIONS, proxy_configuration=proxy_config) + + self.assertTimeouts(self, connection, CONNECT_TIMEOUT, SOCKET_TIMEOUT) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertMaxConnections(self, connection, MAX_CONNECTIONS, proxy_config) + self.assertProxy(self, connection, proxy_config) + + @staticmethod + def assertNoProxy(test_instance, default_connection): + """Asserts that the default_connection does not have any proxy settings contained within""" + test_instance.assertFalse(default_connection._DefaultConnection__requests_session.proxies) + + @staticmethod + def assertProxy(test_instance, connection, proxy_configuration): + """Asserts that the proxy data inside the connection is consistent with the data in proxy_configuration""" + test_instance.assertIn(str(proxy_configuration), + list(connection._DefaultConnection__requests_session.proxies.values())) + + @staticmethod + def assertConnection(test_instance, default_connection, connect_timeout, socket_timeout, + max_connections, proxy_configuration=None): + """Asserts that the default_connection parameter has properties conform + the connect_timeout, the socket_timeout, max_connections and the proxy_configuration + """ + DefaultConnectionTest.assertTimeouts(test_instance, default_connection, connect_timeout, socket_timeout) + DefaultConnectionTest.assertMaxConnections(test_instance, default_connection, max_connections, + proxy_configuration) + if proxy_configuration is not None: + DefaultConnectionTest.assertProxy(test_instance, default_connection, proxy_configuration) + else: + DefaultConnectionTest.assertNoProxy(test_instance, default_connection) + + @staticmethod + def assertTimeouts(test_instance, connection, connection_timeout, socket_timeout): + """Asserts that the settings in the request config of the connection have the proper timeout settings""" + test_instance.assertEqual(connection_timeout, connection.connect_timeout) + test_instance.assertEqual(socket_timeout, connection.socket_timeout) + + @staticmethod + def assertMaxConnections(test_instance, connection, max_connections, proxy_configuration): + """Asserts that the connection has the correct setting for max_connections and proxy_configuration""" + requests_session = connection._DefaultConnection__requests_session + try: + http_poolsize = requests_session.get_adapter("http://")._pool_maxsize + https_poolsize = requests_session.get_adapter("https://")._pool_maxsize + test_instance.assertEqual(http_poolsize, + https_poolsize) # requests stores its poolsize as a per-host variable + except Exception as e: + if isinstance(e, AssertionError): + raise e + else: + print("Could not access max_connections attribute in libary for validation") + + # proxy settings are deeply embedded in requests, we don't check them here + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_default_connection_logger.py b/tests/unit/test_default_connection_logger.py new file mode 100644 index 0000000..4a853de --- /dev/null +++ b/tests/unit/test_default_connection_logger.py @@ -0,0 +1,648 @@ +import os +import re +import time +import unittest +from typing import List + +from requests.exceptions import Timeout + +import tests.file_utils as file_utils +from tests.unit.server_mock_utils import create_server_listening, create_communicator + +from worldline.acquiring.sdk.communication.communication_exception import CommunicationException +from worldline.acquiring.sdk.communication.not_found_exception import NotFoundException +from worldline.acquiring.sdk.communication.param_request import ParamRequest +from worldline.acquiring.sdk.communication.request_param import RequestParam +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.domain.data_object import DataObject +from worldline.acquiring.sdk.log.communicator_logger import CommunicatorLogger + + +class DefaultConnectionLoggerTest(unittest.TestCase): + """Tests that services can operate through DefaultConnection and that their network traffic is appropriately logged + """ + + def setUp(self): + self.request_path = None # Indicating whether or not a request has arrived at the correct server path + self.communicator = None # Stores the communicator used in testing so callbacks can reach it + + def test_get_without_query_params(self): + """Test that a GET service without parameters can connect to a server and is logged appropriately""" + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("getWithoutQueryParams.json") + handler = self.create_handler(body=response_body, + additional_headers=(('Content-type', 'application/json'),)) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + response = communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertIsNotNone(response) + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertEqual('OK', response.content['result']) + self.assertEqual(2, len(logger.entries)) + # for request and response, check that the message exists in the logs and there are no errors + request_entry = logger.entries[0] + self.assertIsNotNone(request_entry[0]) + self.assertIsNone(request_entry[1]) + response_entry = logger.entries[1] + self.assertIsNotNone(response_entry[0]) + self.assertIsNone(response_entry[1]) + # for request and response, check that their output is as predicted and that they match each other + self.assertRequestAndResponse(request_entry[0], response_entry[0], "getWithoutQueryParams") + + def test_get_with_query_params(self): + """Test that a GET service with parameters can connect to a server and is logged appropriately""" + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + query_params = TestParamRequest([ + RequestParam("source", "EUR"), + RequestParam("target", "USD"), + RequestParam("amount", "1000"), + ]) + + response_body = read_resource("getWithQueryParams.json") + handler = self.create_handler(body=response_body, + additional_headers=(('Content-type', 'application/json'),)) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + response = communicator.get('/v1/get', None, query_params, GenericObject, None) + + self.assertIsNotNone(response) + self.assertIsNotNone(response.content['convertedAmount']) + self.assertEqual(test_path, self.request_path.split("?")[0], + 'Request has arrived at {} instead of {}'.format(self.request_path.split("?")[0], test_path)) + self.assertLogsRequestAndResponse(logger, "getWithQueryParams") + + def test_delete_with_void_response(self): + """Test that a POST service without body and a void response can connect to a server and is logged appropriately + """ + test_path = "/v1/void" # relative url through which the request should be sent + logger = TestLogger() + + handler = self.create_handler(response_code=204) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + communicator.delete('/v1/void', None, None, None, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertLogsRequestAndResponse(logger, "deleteWithVoidResponse") + + def test_post_with_created_response(self): + """Test that a POST service with 201 response can connect to a server and is logged appropriately""" + test_path = "/v1/created" # relative url through which the request should be sent + logger = TestLogger() + + request = create_post_request() + + response_body = read_resource("postWithCreatedResponse.json") + additional_headers = (("content-Type", "application/json"), + ("Location", "http://localhost/v1/created/000000123410000595980000100001")) + handler = self.create_handler(response_code=201, body=response_body, + additional_headers=additional_headers) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + response = communicator.post('/v1/created', None, None, request, GenericObject, None) + + self.assertIsNotNone(response) + self.assertIsNotNone(response.content['payment']) + self.assertIsNotNone(response.content['payment']['id']) + self.assertEqual(test_path, self.request_path, + 'Request has arrived at "{1}" while it should have been delivered to "{0}"'.format( + test_path, self.request_path)) + self.assertLogsRequestAndResponse(logger, "postWithCreatedResponse") + + def test_post_with_bad_request_response(self): + """Test that a POST service that is invalid results in an error, which is logged appropriately""" + test_path = "/v1/bad-request" # relative url through which the request should be sent + logger = TestLogger() + + request = create_post_request() + + response_body = read_resource("postWithBadRequestResponse.json") + handler = self.create_handler(response_code=400, body=response_body, + additional_headers=(('Content-type', 'application/json'),)) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + with self.assertRaises(ResponseException): + communicator.post('/v1/bad-request', None, None, request, GenericObject, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertLogsRequestAndResponse(logger, "postWithBadRequestResponse") + + def test_logging_unknown_server_error(self): + """Test that a GET service that results in an error is logged appropriately""" + # reuse the request from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("unknownServerError.json") + handler = self.create_handler(response_code=500, body=response_body, + additional_headers=(('Content-type', 'application/json'),)) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + communicator.enable_logging(logger) + with self.assertRaises(ResponseException): + communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertLogsRequestAndResponse(logger, "getWithoutQueryParams", "unknownServerError") + + def test_non_json(self): + """Test that a GET service that results in a not found error is logged appropriately""" + # reuse the request from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("notFound.html") + handler = self.create_handler(response_code=404, body=response_body, + additional_headers=(("Content-Type", "text/html"),)) + with create_server_listening(handler) as address: # start server to listen to request + with create_communicator(address, connect_timeout=0.500, socket_timeout=0.050) as communicator: + communicator.enable_logging(logger) + with self.assertRaises(NotFoundException): + communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertLogsRequestAndResponse(logger, "getWithoutQueryParams", "notFound") + + def test_read_timeout(self): + """Test that if an exception is thrown before log due to a timeout, it is logged""" + # reuse the request from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("notFound.html") + handler = self.create_handler(response_code=404, body=response_body, + additional_headers=(("Content-Type", "text/html"),)) + + def delayed_response(*args, **kwargs): + time.sleep(0.100) + handler(*args, **kwargs) + + with create_server_listening(delayed_response) as address: # start server to listen to request + with create_communicator(address, socket_timeout=0.05) as communicator: # create communicator under test + communicator.enable_logging(logger) + with self.assertRaises(CommunicationException): + communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertEqual(2, len(logger.entries)) + # for request and response, check that the message exists in the logs and there is an error in the response + request_entry = logger.entries[0] + self.assertIsNotNone(request_entry[0]) + self.assertIsNone(request_entry[1]) + response_entry = logger.entries[1] + self.assertIsNotNone(response_entry[0]) + self.assertIsNotNone(response_entry[1]) + # for request and error, check that their output is as predicted and that they match each other + self.assertRequestAndError(request_entry[0], response_entry[0], "getWithoutQueryParams") + self.assertIsInstance(response_entry[1], Timeout, "logger should have logged a timeout error") + + def test_log_request_only(self): + """Test that a request can be logged separately by disabling log between request and response""" + # reuse the request and response from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("getWithoutQueryParams.json") + handler = self.create_handler(response_code=200, body=response_body, + additional_headers=(('Content-type', 'application/json'),)) + + def disable_logging_response(*args, **kwargs): # handler that disables the log of the communicator + self.communicator.disable_logging() # before responding + handler(*args, **kwargs) + + with create_server_listening(disable_logging_response) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + self.communicator = communicator + communicator.enable_logging(logger) + response = communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual("OK", response.content['result']) + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertEqual(1, len(logger.entries)) + # check that the request message exists in the logs and there are no errors + request_entry = logger.entries[0] + self.assertIsNotNone(request_entry[0]) + self.assertIsNone(request_entry[1], + "Error '{}' logged that should not have been thrown".format(request_entry[1])) + # check that the request is formatted correctly + self.assertRequest(request_entry[0], "getWithoutQueryParams") + + def test_log_response_only(self): + """Test that a response can be logged separately by enabling log between request and response""" + # reuse the request and response from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("getWithoutQueryParams.json") + handler = self.create_handler(response_code=200, body=response_body, + additional_headers=(("Content-Type", "application/json"),)) + + def enable_logging_response(*args, **kwargs): # handler that enables the log of the communicator + self.communicator.enable_logging(logger) # before responding + handler(*args, **kwargs) + + with create_server_listening(enable_logging_response) as address: # start server to listen to request + with create_communicator(address) as communicator: # create communicator under test + self.communicator = communicator + response = communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual("OK", response.content['result']) + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertEqual(1, len(logger.entries)) + # check that the response message exists in the logs and there are no errors + response_entry = logger.entries[0] + self.assertIsNotNone(response_entry[0]) + self.assertIsNone(response_entry[1], + "Error '{}' logged that should not have been thrown".format(response_entry[1])) + # check that the response is formatted correctly + self.assertResponse(response_entry[0], "getWithoutQueryParams") + + def test_log_error_only(self): + """Test that an error can be logged separately by enabling log between request and response""" + # reuse the request from getWithoutQueryParams + test_path = "/v1/get" # relative url through which the request should be sent + logger = TestLogger() + + response_body = read_resource("notFound.html") + handler = self.create_handler(response_code=404, body=response_body, + additional_headers=(("Content-Type", "text/html"),)) + + def enable_logging_late_response(*args, **kwargs): # handler that enables the log of the communicator + self.communicator.enable_logging(logger) # and waits for a timeout before responding + time.sleep(0.1) + handler(*args, **kwargs) + + with create_server_listening(enable_logging_late_response) as address: # start server to listen to request + with create_communicator(address, connect_timeout=0.500, socket_timeout=0.050) as communicator: + self.communicator = communicator + with self.assertRaises(CommunicationException): + communicator.get('/v1/get', None, None, GenericObject, None) + + self.assertEqual(test_path, self.request_path, 'Request has arrived at the wrong path') + self.assertEqual(1, len(logger.entries)) + # check that the response message exists in the logs and there are no errors + error_entry = logger.entries[0] + self.assertIsNotNone(error_entry[0]) + self.assertIsNotNone(error_entry[1]) + # check that the error is formatted correctly + self.assertError(error_entry[0]) + self.assertIsInstance(error_entry[1], Timeout, + "logger should have logged a timeout error, logged {} instead".format(error_entry[1])) + + def assertLogsRequestAndResponse(self, logger, request_resource_prefix, response_resource_prefix=None): + """Assert that the logs of the logger contain both request and response and no errors, + then check that the request and response match using "assertRequestAndResponse" + """ + if response_resource_prefix is None: + response_resource_prefix = request_resource_prefix + self.assertEqual(2, len(logger.entries)) + # for request and response, check that the message exists in the logs and there are no errors + request_entry = logger.entries[0] + self.assertIsNotNone(request_entry[0]) + self.assertIsNone(request_entry[1], + "Error '{}' logged that should not have been thrown".format(request_entry[1])) + response_entry = logger.entries[1] + self.assertIsNotNone(response_entry[0]) + self.assertIsNone(response_entry[1], + "Error '{}' logged that should not have been thrown".format(response_entry[1])) + # for request and response, check that their output is as predicted and that they match each other + self.assertRequestAndResponse(request_entry[0], response_entry[0], + request_resource_prefix, response_resource_prefix) + + def assertRequestAndResponse(self, request_message, response_message, + request_resource_prefix, response_resource_prefix=None): + """Assert that the request and response messages match the request and response regular expressions stored in + 'request_resource_prefix'.request and 'response_resource_prefix'.response respectively. + + If response_resource_prefix is not given it is assumed to be equal to request_resource_prefix""" + if response_resource_prefix is None: + response_resource_prefix = request_resource_prefix + request_id = self.assertRequest(request_message, request_resource_prefix) + self.assertResponse(response_message, response_resource_prefix, request_id) + + def assertRequestAndError(self, request_message, error_message, resource_prefix): + """Assert that the request message matches the request regular expression stored in 'resource_prefix.request' + and the error is a valid error message and refers to the request""" + request_id = self.assertRequest(request_message, resource_prefix) + self.assertError(error_message, request_id) + + def assertRequest(self, request_message, request_resource_prefix): + """Assert that the request message matches the regex stored in 'request_resource_prefix'.request + + :param request_message: the request message to match + :param request_resource_prefix: prefix of the regex file location, + it will be appended with '.request' to obtain the file location + :return: the request_id for use in matching the corresponding response or error + """ + request_resource = request_resource_prefix + "_request" + regex = globals()[request_resource](request_message, self) + if type(regex) == type(""): + request_pattern = re.compile(regex, re.DOTALL) + match = request_pattern.match(request_message.get_message()) + print(globals()[request_resource]) + if match is None: + raise AssertionError("request message '" + request_message.get_message() + + "' does not match pattern " + str(request_pattern)) + self.assertRegex(request_message, request_pattern) + return match.group(1) + return regex[0] + + def assertResponse(self, response_message, response_resource_prefix, request_id=None): + """Assert that the response message matches the regex stored in 'response_resource_prefix'.response + + :param response_message: the response message to match + :param response_resource_prefix: prefix of the regex file location, + it will be appended with '.response' to obtain the file location + :param request_id: if a request_id is provided, it is matched against the response_id found in the response, + failing the assert if not equal + """ + response_resource = response_resource_prefix + "_response" + # for each response call the corresponding asserting function + regex = globals()[response_resource](response_message, self) + if type(regex) == type(""): + response_pattern = re.compile(regex, re.DOTALL) + match = response_pattern.match(response_message.get_message()) + if match is None: + raise AssertionError("response message '" + response_message.get_message() + + "' does not match pattern " + str(response_pattern)) + if request_id is not None: + self.assertEqual(request_id, match.group(1), + "request_id '{0}' does not match response_id '{1}'".format(request_id, match.group(1))) + + def assertError(self, error_message, request_id=None): + """Assert that the error message matches the regex stored in 'generic.error' + + :param error_message: the error message to match + :param request_id: if a request_id is provided, it is matched against the error_id found in the error, + failing the assert if not equal + """ + error_resource = "generic_error" + error_pattern_string = globals()[error_resource]() + error_pattern = re.compile(error_pattern_string, re.DOTALL) + match = error_pattern.match(error_message) + if match is None: + raise AssertionError("response message '" + error_message + + "' does not match pattern " + str(error_pattern_string)) + if request_id is not None: + self.assertEqual(request_id, match.group(1), + "request_id '{0}' does not match error_id '{1}'".format(request_id, match.group(1))) + + def assertHeaderIn(self, _tuple, _list): + # If tuple has incorrect number of elements, assume it is not in the list + self.assertIn((_tuple[0].lower(), _tuple[1]), + list(map((lambda el: (el[0].lower(), el[1])), _list))) + + def create_handler(self, response_code=200, body='', # path='', + additional_headers=()): + """Creates a request handler that receives the request on the server side + + :param response_code: status code of the desired response + :param body: the body of the response message to return, it should be in json format + :param additional_headers: additional headers that are added to the handler's response + If the request is sent through the proper path, self.request_successful will be set to true, false otherwise + """ + + def handler_func(handler): + self.request_path = handler.path # record if the request was sent through the expected path + handler.protocol_version = 'HTTP/1.1' + try: + handler.send_response(response_code) + for header in additional_headers: + handler.send_header(*header) + handler.send_header('Dummy', None) + handler.send_header('Content-Length', len(bytes(body, "utf-8"))) + handler.end_headers() + handler.wfile.write(bytes(body, "utf-8")) + except ConnectionAbortedError: + pass + + return handler_func + + +def create_post_request(): + """Creates a commonly used request for testing""" + return {'card': {'cardSecurityCode': '123', 'cardNumber': '1234567890123456', 'expiryDate': '122024'}} + + +class TestLogger(CommunicatorLogger): + def __init__(self): + CommunicatorLogger.__init__(self) + self.entries = [] + + def log_request(self, request_log_message): + self.entries.append((request_log_message, None)) + + def log_response(self, response_log_message): + self.entries.append((response_log_message, None)) + + def log(self, message, thrown=None): + self.entries.append((message, thrown)) + + +class GenericObject(DataObject): + content: dict = {} + + def to_dictionary(self) -> dict: + return self.content + + def from_dictionary(self, dictionary: dict) -> 'DataObject': + self.content = dictionary + return self + + +class TestParamRequest(ParamRequest): + + def __init__(self, params: List[RequestParam]): + self.params = params + + def to_request_parameters(self) -> List[RequestParam]: + return self.params + + +# reads a file names file_name stored under resources/communication +def read_resource(file_name): return file_utils.read_file(os.path.join("communication", file_name)) + + +# ------------------------ REGEX SOURCES ------------------ + + +def getWithQueryParams_request(request, test): + test.assertEqual(request.method, "GET") + test.assertEqual(request.uri, '/v1/get?source=EUR&target=USD&amount=1000') + + headers = request.get_header_list() + test.assertHeaderIn(('Authorization', '"********"'), headers) + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertTrue(len(list(filter((lambda header: header[0] == 'X-WL-ServerMetaInfo'), headers)))) + + test.assertIsNone(request.body) + + return request.request_id, False + + +def getWithQueryParams_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 200) + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertEqual(response.content_type, 'application/json') + test.assertIsNotNone(response.body) + test.assertTrue(len(response.body)) + return response.request_id, False + + +def postWithBadRequestResponse_request(request, test): + test.assertEqual(request.method, "POST") + test.assertEqual(request.uri, '/v1/bad-request') + + headers = request.get_header_list() + test.assertHeaderIn(('Authorization', '"********"'), headers) + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertTrue(len(list(filter((lambda header: header[0] == 'X-WL-ServerMetaInfo'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + + test.assertEqual(request.content_type, 'application/json') + + test.assertIsNotNone(request.body) + test.assertTrue(len(request.body)) + + return request.request_id, False + + +def postWithBadRequestResponse_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 400) + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertEqual(response.content_type, 'application/json') + test.assertIsNotNone(response.body) + test.assertTrue(len(response.body)) + return response.request_id, False + + +def postWithCreatedResponse_request(request, test): + test.assertEqual(request.method, "POST") + test.assertEqual(request.uri, '/v1/created') + + headers = request.get_header_list() + test.assertHeaderIn(('Authorization', '"********"'), headers) + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertTrue(len(list(filter((lambda header: header[0] == 'X-WL-ServerMetaInfo'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + + test.assertEqual(request.content_type, 'application/json') + + test.assertIsNotNone(request.body) + test.assertTrue(len(request.body)) + + return request.request_id, False + + +def postWithCreatedResponse_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 201) + test.assertEqual(response.content_type, 'application/json') + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertHeaderIn(('Location', '"http://localhost/v1/created/000000123410000595980000100001"'), headers) + test.assertIsNotNone(response.body) + test.assertTrue(len(response.body)) + return response.request_id, False + + +def deleteWithVoidResponse_request(request, test): + test.assertEqual(request.method, "DELETE") + test.assertEqual(request.uri, '/v1/void') + + headers = request.get_header_list() + test.assertHeaderIn(('Authorization', '"********"'), headers) + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertTrue(len(list(filter((lambda header: header[0] == 'X-WL-ServerMetaInfo'), headers)))) + + return request.request_id, False + + +def deleteWithVoidResponse_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 204) + test.assertIsNone(response.content_type) + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertIsNone(response.body) + return response.request_id, False + + +def generic_error(): + return r"""Error\ occurred\ for\ outgoing\ request\ \(requestId\=\'([-a-zA-Z0-9]+)\'\,\ \d+\ s\)""" + + +def notFound_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 404) + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"text/html"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertEqual(response.content_type, 'text/html') + test.assertIsNotNone(response.body) + test.assertEqual(response.body, "Not Found") + return response.request_id, False + + +def getWithoutQueryParams_request(request, test): + test.assertEqual(request.method, "GET") + test.assertEqual(request.uri, '/v1/get') + + headers = request.get_header_list() + test.assertHeaderIn(('Authorization', '"********"'), headers) + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertTrue(len(list(filter((lambda header: header[0] == 'X-WL-ServerMetaInfo'), headers)))) + + return request.request_id, False + + +def getWithoutQueryParams_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 200) + test.assertEqual(response.content_type, 'application/json') + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertIsNotNone(response.body) + test.assertTrue(len(response.body)) + return response.request_id, False + + +def unknownServerError_response(response, test): + test.assertIsNotNone(response.get_duration()) + test.assertEqual(response.get_status_code(), 500) + headers = response.get_header_list() + test.assertTrue(len(list(filter((lambda header: header[0] == 'Date'), headers)))) + test.assertHeaderIn(('Content-Type', '"application/json"'), headers) + test.assertHeaderIn(('Dummy', '""'), headers) + test.assertEqual(response.content_type, 'application/json') + test.assertIsNotNone(response.body) + test.assertTrue(len(response.body)) + return response.request_id, False + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_default_marshaller.py b/tests/unit/test_default_marshaller.py new file mode 100644 index 0000000..2fc9732 --- /dev/null +++ b/tests/unit/test_default_marshaller.py @@ -0,0 +1,126 @@ +import unittest + +from worldline.acquiring.sdk.domain.data_object import DataObject +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + + +class DefaultMarshallerTest(unittest.TestCase): + """Tests that the default marshaller is able to marshal an object + and unmarshal that same object to a more generic version + """ + + def test_unmarshal_with_extra_fields(self): + """Tests if the marshaller is able to marshal an object and unmarshal it as an instance of a parent class""" + dummy_object = JsonDummyExtended() + mini_dummy = JsonMiniDummy() + mini_mini_dummy = JsonMiniMiniDummy() + mini_mini_dummy.foo = "hiddenfoo" + mini_dummy.foo = mini_mini_dummy + dummy_object.foo = mini_dummy + dummy_object.bar = True + dummy_object.boo = 0o1 + dummy_object.far = "close" + dummy_object.extra_field = "something else" + marshaller = DefaultMarshaller.instance() + + json = marshaller.marshal(dummy_object) + unmarshalled = marshaller.unmarshal(json, JsonDummy) + + self.assertEqual(True, unmarshalled.bar) + self.assertEqual(0o1, unmarshalled.boo) + self.assertEqual("close", unmarshalled.far) + self.assertEqual("hiddenfoo", unmarshalled.foo.foo.foo) + + +# --------------- A number of dummy objects for testing ------------- + +class JsonMiniMiniDummy(DataObject): + foo = "standardfoo" + + def to_dictionary(self): + dictionary = super(JsonMiniMiniDummy, self).to_dictionary() + if self.foo is not None: + dictionary['foo'] = self.foo + + return dictionary + + def from_dictionary(self, dictionary): + super(JsonMiniMiniDummy, self).from_dictionary(dictionary) + if 'foo' in dictionary: + self.foo = dictionary['foo'] + return self + + +class JsonMiniDummy(DataObject): + foo = JsonMiniMiniDummy() + + def to_dictionary(self): + dictionary = super(JsonMiniDummy, self).to_dictionary() + if self.foo is not None: + dictionary['foo'] = self.foo.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary): + super(JsonMiniDummy, self).from_dictionary(dictionary) + if 'foo' in dictionary: + if not isinstance(dictionary['foo'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['foo'])) + value = JsonMiniMiniDummy() + self.foo = value.from_dictionary(dictionary['foo']) + return self + + +class JsonDummy(DataObject): + foo = JsonMiniDummy() + bar = False + boo = 00 + far = "far" + + def to_dictionary(self): + dictionary = super(JsonDummy, self).to_dictionary() + if self.foo is not None: + dictionary['foo'] = self.foo + if self.bar is not None: + dictionary['bar'] = self.bar + if self.boo is not None: + dictionary['boo'] = self.boo + if self.far is not None: + dictionary['far'] = self.far + + return dictionary + + def from_dictionary(self, dictionary): + super(JsonDummy, self).from_dictionary(dictionary) + if 'foo' in dictionary: + if not isinstance(dictionary['foo'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['foo'])) + value = JsonMiniDummy() + self.foo = value.from_dictionary(dictionary['foo']) + if 'bar' in dictionary: + self.bar = dictionary['bar'] + if 'boo' in dictionary: + self.boo = dictionary['boo'] + if 'far' in dictionary: + self.far = dictionary['far'] + + return self + + +class JsonDummyExtended(JsonDummy): + extra_field = "something something" + + def to_dictionary(self): + dictionary = super(JsonDummyExtended, self).to_dictionary() + if self.extra_field is not None: + dictionary['extraField'] = self.extra_field + return dictionary + + def from_dictionary(self, dictionary): + super(JsonDummyExtended, self).from_dictionary(dictionary) + if 'extraField' in dictionary: + self.far = dictionary['extraField'] + return self + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_factory.py b/tests/unit/test_factory.py new file mode 100644 index 0000000..6d778ae --- /dev/null +++ b/tests/unit/test_factory.py @@ -0,0 +1,74 @@ +import os +import unittest +import warnings + +from urllib.parse import urlparse + +from tests.unit.test_default_connection import DefaultConnectionTest + +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.authentication.authorization_type import AuthorizationType +from worldline.acquiring.sdk.authentication.oauth2_authenticator import OAuth2Authenticator +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + +PROPERTIES_URI_OAUTH2 = os.path.abspath(os.path.join(__file__, os.pardir, "../resources/configuration.oauth2.ini")) +OAUTH2_CLIENT_ID = "someId" +OAUTH2_CLIENT_SECRET = "someSecret" + + +class FactoryTest(unittest.TestCase): + """Tests that the factory is capable of correctly creating communicators and communicator configurations""" + + def test_create_configuration(self): + """Tests that the factory is correctly able to create a communicator configuration""" + configuration = Factory.create_configuration(PROPERTIES_URI_OAUTH2, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET) + self.assertEqual(urlparse("https://api.preprod.acquiring.worldline-solutions.com"), configuration.api_endpoint) + self.assertEqual(AuthorizationType.get_authorization("OAuth2"), configuration.authorization_type) + self.assertEqual(1000, configuration.connect_timeout) + self.assertEqual(1000, configuration.socket_timeout) + self.assertEqual(100, configuration.max_connections) + self.assertEqual(OAUTH2_CLIENT_ID, configuration.oauth2_client_id) + self.assertEqual(OAUTH2_CLIENT_SECRET, configuration.oauth2_client_secret) + self.assertIsNone(configuration.proxy_configuration) + + # noinspection PyUnresolvedReferences,PyUnresolvedReferences,PyUnresolvedReferences + def test_create_communicator(self): + """Tests that the factory is correctly able to create a communicator""" + communicator = Factory.create_communicator_from_file(PROPERTIES_URI_OAUTH2, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET) + + self.assertIs(communicator.marshaller, DefaultMarshaller.instance()) + + connection = communicator._Communicator__connection + self.assertIsInstance(connection, DefaultConnection) + DefaultConnectionTest.assertConnection(self, connection, 1000, 1000, 100, None) + + authenticator = communicator._Communicator__authenticator + self.assertIsInstance(authenticator, OAuth2Authenticator) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertEqual(OAUTH2_CLIENT_ID, authenticator._OAuth2Authenticator__client_id) + self.assertEqual(OAUTH2_CLIENT_SECRET, authenticator._OAuth2Authenticator__client_secret) + + metadata_provider = communicator._Communicator__metadata_provider + self.assertIsInstance(metadata_provider, MetadataProvider) + request_headers = metadata_provider.metadata_headers + self.assertEqual(1, len(request_headers)) + self.assertEqual("X-WL-ServerMetaInfo", request_headers[0].name) + + # noinspection PyUnresolvedReferences,PyUnresolvedReferences,PyUnresolvedReferences + def test_create_communicator_with_authorization_type_oauth2(self): + """Tests that the factory is correctly able to create a communicator""" + communicator = Factory.create_communicator_from_file(PROPERTIES_URI_OAUTH2, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET) + + authenticator = communicator._Communicator__authenticator + self.assertIsInstance(authenticator, OAuth2Authenticator) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertEqual(OAUTH2_CLIENT_ID, authenticator._OAuth2Authenticator__client_id) + self.assertEqual(OAUTH2_CLIENT_SECRET, authenticator._OAuth2Authenticator__client_secret) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_header_obfuscator.py b/tests/unit/test_header_obfuscator.py new file mode 100644 index 0000000..7b7f4ed --- /dev/null +++ b/tests/unit/test_header_obfuscator.py @@ -0,0 +1,68 @@ +import os +import unittest + +from tests import file_utils + +from worldline.acquiring.sdk.log.header_obfuscator import HeaderObfuscator +from worldline.acquiring.sdk.log.obfuscation_rule import obfuscate_all + + +class HeaderObfuscatorTest(unittest.TestCase): + """Tests if the header obfuscator is capable of obfuscating headers of requests""" + + def test_obfuscate_header(self): + """Tests that any default headers get obfuscated, while others do not""" + self.obfuscate_header_match("Authorization", + "Basic QWxhZGRpbjpPcGVuU2VzYW1l", + "********") + self.obfuscate_header_match("authorization", + "Basic QWxhZGRpbjpPcGVuU2VzYW1l", + "********") + self.obfuscate_header_match("AUTHORIZATION", + "Basic QWxhZGRpbjpPcGVuU2VzYW1l", + "********") + + self.obfuscate_header_no_match("Content-Type", "application/json") + self.obfuscate_header_no_match("content-type", "application/json") + self.obfuscate_header_no_match("CONTENT-TYPE", "application/json") + + def test_obfuscate_custom_header(self): + """Tests that any default and custom headers get obfuscated""" + header_obfuscator = HeaderObfuscator(additional_rules={ + "content-type": obfuscate_all() + }) + + self.obfuscate_header_match("Authorization", "Basic QWxhZGRpbjpPcGVuU2VzYW1l", "********", + header_obfuscator=header_obfuscator) + self.obfuscate_header_match("authorization", "Basic QWxhZGRpbjpPcGVuU2VzYW1l", "********", + header_obfuscator=header_obfuscator) + self.obfuscate_header_match("AUTHORIZATION", "Basic QWxhZGRpbjpPcGVuU2VzYW1l", "********", + header_obfuscator=header_obfuscator) + + self.obfuscate_header_match("Content-Type", "application/json", "****************", + header_obfuscator=header_obfuscator) + self.obfuscate_header_match("content-type", "application/json", "****************", + header_obfuscator=header_obfuscator) + self.obfuscate_header_match("CONTENT-TYPE", "application/json", "****************", + header_obfuscator=header_obfuscator) + + def obfuscate_header_match(self, name, original_value, + expected_obfuscated_value, + header_obfuscator=HeaderObfuscator.default_header_obfuscator()): + """Tests that the obfuscator obfuscates the original_value to produce the expected_obfuscated_value""" + obfuscated_value = header_obfuscator.obfuscate_header(name, original_value) + self.assertEqual(expected_obfuscated_value, obfuscated_value) + + def obfuscate_header_no_match(self, name, original_value): + """Tests that the obfuscator leaves the parameter header intact and does not obfuscate anything""" + obfuscated_value = HeaderObfuscator.default_header_obfuscator().obfuscate_header(name, original_value) + self.assertEqual(original_value, obfuscated_value) + + +# reads a file names file_name stored under resources/log +def _read_resource(file_name): return file_utils.read_file( + os.path.join("log", file_name)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_metadata_provider.py b/tests/unit/test_metadata_provider.py new file mode 100644 index 0000000..261c8ce --- /dev/null +++ b/tests/unit/test_metadata_provider.py @@ -0,0 +1,89 @@ +# -*- coding: UTF-8 -*- +import base64 +import unittest + +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.communication.request_header import RequestHeader +from worldline.acquiring.sdk.domain.shopping_cart_extension import ShoppingCartExtension +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + + +class MetadataProviderTest(unittest.TestCase): + """Contains tests to check that the metadata provider correctly stores allowed request headers + and refuses prohibited headers + """ + def test_server_metadata_headers_full(self): + """Tests that the MetadataProvider can construct metadata_headers when supplied with a full shopping cart""" + shopping_cart_extension = ShoppingCartExtension("Worldline.creator", "Extension", "1.0", "ExtensionId") + metadata_provider = MetadataProvider("Worldline", shopping_cart_extension) + + request_headers = metadata_provider.metadata_headers + self.assertEqual(1, len(request_headers)) + self.assertServerMetaInfo(metadata_provider, "Worldline", shopping_cart_extension, request_headers[0]) + + def test_server_metadata_headers_full_no_shopping_cart_extension_id(self): + """Tests that the MetadataProvider can construct metadata_headers when supplied with a full shopping cart""" + shopping_cart_extension = ShoppingCartExtension("Worldline.creator", "Extension", "1.0") + metadata_provider = MetadataProvider("Worldline", shopping_cart_extension) + + request_headers = metadata_provider.metadata_headers + self.assertEqual(1, len(request_headers)) + self.assertServerMetaInfo(metadata_provider, "Worldline", shopping_cart_extension, request_headers[0]) + + def test_get_server_metadata_headers_no_additional_headers(self): + """Tests that the MetadataProvider functions correctly without any additional headers as arguments""" + metadata_provider = MetadataProvider("Worldline") + + request_headers = metadata_provider.metadata_headers + self.assertEqual(1, len(request_headers)) + self.assertServerMetaInfo(metadata_provider, "Worldline", None, request_headers[0]) + + def test_get_server_metadata_headers_additional_headers(self): + """Tests that the MetadataProvider can handle multiple additional headers""" + additional_headers = [RequestHeader("Header1", "&=$%"), RequestHeader("Header2", "blah blah"), + RequestHeader("Header3", "foo")] + metadata_provider = MetadataProvider("Worldline", None, additional_headers) + request_headers = metadata_provider.metadata_headers + + self.assertEqual(4, len(request_headers)) + + for index in range(1, 4): + self.assertEqual(additional_headers[index-1].name, request_headers[index].name) + self.assertEqual(additional_headers[index-1].value, request_headers[index].value) + + def test_constructor_with_prohibited_headers(self): + """Tests that the MetadataProvider constructor does not accept any headers marked as prohibited""" + for name in MetadataProvider.prohibited_headers: + additional_headers = [RequestHeader("Header1", "Value1"), + RequestHeader(name, "should be slashed and burnt"), + RequestHeader("Header3", "Value3")] + with self.assertRaises(Exception) as error: + MetadataProvider("Worldline", None, additional_headers) + self.assertIn(name, str(error.exception)) + + def assertServerMetaInfo(self, metadata_provider, integrator, shopping_cart_extension=None, request_header=None): + """Assert that checks that the request_header is the default header "X-WL-ServerMetaInfo", + that the server_metadata_info of the metadata_provider is correct + and that the shopping cart extension is consistent with the extension stored in metadata_provider + """ + self.assertEqual("X-WL-ServerMetaInfo", request_header.name) + self.assertIsNotNone(request_header.value) + + # server_meta_info is stored in json format and encoded using utf-8 and base64 encoding, decode it + server_meta_info_json = base64.b64decode(request_header.value).decode('utf-8') + server_meta_info = DefaultMarshaller.instance().unmarshal(server_meta_info_json, MetadataProvider.ServerMetaInfo) + self.assertEqual(metadata_provider._platform_identifier, server_meta_info.platform_identifier) + self.assertEqual(metadata_provider._sdk_identifier, server_meta_info.sdk_identifier) + self.assertEqual("Worldline", server_meta_info.sdk_creator) + self.assertEqual(integrator, server_meta_info.integrator) + if shopping_cart_extension is None: + self.assertIsNone(server_meta_info.shopping_cart_extension) + else: + self.assertEqual(shopping_cart_extension.creator, server_meta_info.shopping_cart_extension.creator) + self.assertEqual(shopping_cart_extension.name, server_meta_info.shopping_cart_extension.name) + self.assertEqual(shopping_cart_extension.version, server_meta_info.shopping_cart_extension.version) + self.assertEqual(shopping_cart_extension.extension_id, server_meta_info.shopping_cart_extension.extension_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_oauth2_authenticator.py b/tests/unit/test_oauth2_authenticator.py new file mode 100644 index 0000000..42d0c94 --- /dev/null +++ b/tests/unit/test_oauth2_authenticator.py @@ -0,0 +1,93 @@ +import os +import unittest +import uuid +import tests.file_utils as file_utils + +from urllib.parse import urlparse + +from tests.unit.server_mock_utils import create_server_listening +from tests.unit.test_factory import PROPERTIES_URI_OAUTH2 + +from worldline.acquiring.sdk.factory import Factory +from worldline.acquiring.sdk.authentication.oauth2_authenticator import OAuth2Authenticator +from worldline.acquiring.sdk.authentication.oauth2_exception import OAuth2Exception + + +class OAuth2AuthenticatorTest(unittest.TestCase): + """Tests that the OAuth2Authenticator is capable of providing bearer tokens""" + + def setUp(self): + self.configuration = Factory.create_configuration(PROPERTIES_URI_OAUTH2, str(uuid.uuid4()), str(uuid.uuid4())) + self.configuration.connect_timeout = 1000 + self.configuration.socket_timeout = 1000 + + self.call_count = 0 + + def test_authentication_success(self): + response_body = read_resource("oauth2AccessToken.json") + handler = self.create_handler(response_code=200, body=response_body) + with create_server_listening(handler) as address: # start server to listen to request + self.configuration.oauth2_token_uri = address + "/auth/realms/api/protocol/openid-connect/token" + + authenticator = OAuth2Authenticator(self.configuration) + + for _ in range(0, 3): + authorization = authenticator.get_authorization(None, urlparse("http://domain/local/anything/operations"), None) + + self.assertEqual("Bearer accessToken", authorization) + + self.assertEqual(1, self.call_count) + + def test_authentication_invalid_client(self): + response_body = read_resource("oauth2AccessToken.invalidClient.json") + handler = self.create_handler(response_code=401, body=response_body) + with create_server_listening(handler) as address: # start server to listen to request + self.configuration.oauth2_token_uri = address + "/auth/realms/api/protocol/openid-connect/token" + + authenticator = OAuth2Authenticator(self.configuration) + + for _ in range(0, 3): + with self.assertRaises(OAuth2Exception) as cm: + authenticator.get_authorization(None, urlparse("http://domain/local/anything/operations"), None) + + oauth2_exception = cm.exception + self.assertEqual( + "There was an error while retrieving the OAuth2 access token: unauthorized_client - INVALID_CREDENTIALS: Invalid client credentials", + str(oauth2_exception)) + + self.assertEqual(3, self.call_count) + + def test_authentication_expired_token(self): + response_body = read_resource("oauth2AccessToken.expired.json") + handler = self.create_handler(response_code=200, body=response_body) + with create_server_listening(handler) as address: # start server to listen to request + self.configuration.oauth2_token_uri = address + "/auth/realms/api/protocol/openid-connect/token" + + authenticator = OAuth2Authenticator(self.configuration) + + for _ in range(0, 3): + authorization = authenticator.get_authorization(None, urlparse("http://domain/local/anything/operations"), None) + + self.assertEqual("Bearer expiredAccessToken", authorization) + + self.assertEqual(3, self.call_count) + + def create_handler(self, response_code=200, body='{}'): + """ + Creates a request handler that receives the request on the server side + + :param response_code: status code of the desired response + :param body: the body of the response message to return, it should be in json format + """ + def handler_func(handler): + handler.protocol_version = 'HTTP/1.1' + handler.send_response(response_code) + handler.send_header('Content-Type', 'application/json') + handler.end_headers() + handler.wfile.write(bytes(body, "utf-8")) + self.call_count = self.call_count + 1 + return handler_func + + +def read_resource(relative_path): + return file_utils.read_file(os.path.join("authentication", relative_path)) diff --git a/tests/unit/test_python_communication_logger.py b/tests/unit/test_python_communication_logger.py new file mode 100644 index 0000000..0a0eb74 --- /dev/null +++ b/tests/unit/test_python_communication_logger.py @@ -0,0 +1,63 @@ +import logging +import unittest + +from worldline.acquiring.sdk.log.python_communicator_logger import PythonCommunicatorLogger + + +class SDKCommunicatorLoggerTest(unittest.TestCase): + """Contains tests to test log normal messages and exceptions using the CommunicatorLogger""" + + def test_log(self): + """Tests that the Python communicator logger appropriately logs messages""" + logger = logging.getLogger(self.__class__.__name__) + logger.setLevel(logging.INFO) + handler = TestHandler() + logger.addHandler(handler) + + communicator_logger = PythonCommunicatorLogger(logger, logging.INFO, logging.WARNING) + communicator_logger.log("test 123") + + self.assertEqual(1, len(handler.records), + "The communication logger should have logged one message, it logged " + + str(len(handler.records))) + record = handler.records[0] + self.assertEqual("test 123", record.msg) + self.assertEqual('INFO', record.levelname) + self.assertEqual(self.__class__.__name__, record.name) + # self.assertEqual(JdkCommunicatorLogger.__module__, record.module) + self.assertEqual("log", record.funcName) + self.assertIsNone(record.exc_info) + + def test_log_exception(self): + """Tests that the Python communicator logger appropriately logs exceptions""" + logger = logging.Logger(self.__class__.__name__) + handler = TestHandler() + logger.addHandler(handler) + + communicator_logger = PythonCommunicatorLogger(logger, logging.INFO, logging.WARNING) + exception = Exception("foo") + communicator_logger.log("test 112", exception) + + self.assertEqual(1, len(handler.records)) + record = handler.records[0] + self.assertEqual("test 112", record.msg) + self.assertEqual('WARNING', record.levelname) + self.assertEqual(self.__class__.__name__, record.name) + # self.assertEqual(JdkCommunicatorLogger.__module__, record.module) + self.assertEqual("log", record.funcName) + self.assertIs(exception, record.args[0], "Exception sbould be recorded in record.args[0]") + + +class TestHandler(logging.NullHandler): + """Test handler that records any logs in a list for testing""" + + def __init__(self): + super(TestHandler, self).__init__() + self.records = [] + + def handle(self, record): + self.records.append(record) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_request_header.py b/tests/unit/test_request_header.py new file mode 100644 index 0000000..a3479ea --- /dev/null +++ b/tests/unit/test_request_header.py @@ -0,0 +1,46 @@ +import unittest + +from worldline.acquiring.sdk.communication.request_header import RequestHeader, get_header, get_header_value + + +class RequestHeaderTest(unittest.TestCase): + + def test_get_header_value_none(self): + """Tests get_header_value using None as headers""" + self.assertIsNone(get_header_value(None, "Content-Length")) + + def test_get_header_value_list(self): + """Tests get_header_value using a list of RequestHeader objects""" + headers = [RequestHeader("Content-Type", "application/json")] + self.assertEqual("application/json", get_header_value(headers, "Content-Type")) + self.assertEqual("application/json", get_header_value(headers, "content-type")) + self.assertIsNone(get_header_value(headers, "Content-Length")) + + def test_get_header_value_dict(self): + """Tests get_header_value using a dictionary""" + headers = {"Content-Type": "application/json"} + self.assertEqual("application/json", get_header_value(headers, "Content-Type")) + self.assertEqual("application/json", get_header_value(headers, "content-type")) + self.assertIsNone(get_header_value(headers, "Content-Length")) + + def test_get_header_none(self): + """Tests get_header using None as headers""" + self.assertIsNone(get_header(None, "Content-Length")) + + def test_get_header_list(self): + """Tests get_header using a list of RequestHeader objects""" + headers = [RequestHeader("Content-Type", "application/json")] + self.assertEqual("Content-Type:application/json", str(get_header(headers, "Content-Type"))) + self.assertEqual("Content-Type:application/json", str(get_header(headers, "content-type"))) + self.assertIsNone(get_header_value(headers, "Content-Length")) + + def test_get_header_dict(self): + """Tests get_header using a dictionary""" + headers = {"Content-Type": "application/json"} + self.assertEqual("Content-Type:application/json", str(get_header(headers, "Content-Type"))) + self.assertEqual("Content-Type:application/json", str(get_header(headers, "content-type"))) + self.assertIsNone(get_header(headers, "Content-Length")) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_response_header.py b/tests/unit/test_response_header.py new file mode 100644 index 0000000..c790f44 --- /dev/null +++ b/tests/unit/test_response_header.py @@ -0,0 +1,55 @@ +import unittest + +from worldline.acquiring.sdk.communication.response_header import get_header, get_header_value, get_disposition_filename + + +class ResponseHeaderTest(unittest.TestCase): + + def test_get_header_value(self): + """Tests get_header_value""" + headers = {"Content-Type": "application/json"} + self.assertEqual("application/json", get_header_value(headers, "Content-Type")) + self.assertEqual("application/json", get_header_value(headers, "content-type")) + self.assertIsNone(get_header_value(headers, "Content-Length")) + + def test_get_header(self): + """Tests get_header_value""" + headers = {"Content-Type": "application/json"} + self.assertEqual(("Content-Type", "application/json"), get_header(headers, "Content-Type")) + self.assertEqual(("Content-Type", "application/json"), get_header(headers, "content-type")) + self.assertIsNone(get_header(headers, "Content-Length")) + + def test_get_disposition_filename(self): + """Tests that get_disposition_filename works as expected""" + test_data = {"attachment; filename=testfile": "testfile", + "attachment; filename=\"testfile\"": "testfile", + "attachment; filename=\"testfile": "\"testfile", + "attachment; filename=testfile\"": "testfile\"", + "attachment; filename='testfile'": "testfile", + "attachment; filename='testfile": "'testfile", + "attachment; filename=testfile'": "testfile'", + "filename=testfile": "testfile", + "filename=\"testfile\"": "testfile", + "filename=\"testfile": "\"testfile", + "filename=testfile\"": "testfile\"", + "filename='testfile'": "testfile", + "filename='testfile": "'testfile", + "filename=testfile'": "testfile'", + "attachment; filename=testfile; x=y": "testfile", + "attachment; filename=\"testfile\"; x=y": "testfile", + "attachment; filename=\"testfile; x=y": "\"testfile", + "attachment; filename=testfile\"; x=y": "testfile\"", + "attachment; filename='testfile'; x=y": "testfile", + "attachment; filename='testfile; x=y": "'testfile", + "attachment; filename=testfile'; x=y": "testfile'", + "attachment": None, + "filename=\"": "\"", + "filename='": "'"} + + for value, expected in test_data.items(): + headers = {"Content-Disposition": value} + self.assertEqual(expected, get_disposition_filename(headers)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_sysout_communicator_logger.py b/tests/unit/test_sysout_communicator_logger.py new file mode 100644 index 0000000..971dfca --- /dev/null +++ b/tests/unit/test_sysout_communicator_logger.py @@ -0,0 +1,51 @@ +import io +import re +import sys +import unittest + +from worldline.acquiring.sdk.log.sys_out_communicator_logger import SysOutCommunicatorLogger + + +class SysOutCommunicatorLoggerTest(unittest.TestCase): + """Test if the SysOutCommunicatorLogger appropriately logs messages and exceptions""" + + def setUp(self): + self.stdout = sys.stdout + sys.stdout = self.mock_io = io.StringIO() + + def tearDown(self): + sys.stdout = self.stdout + self.mock_io.close() + + def test_log(self): + """Test if the SysOutCommunicatorLogger correctly logs a message""" + + logger = SysOutCommunicatorLogger.instance() + logger.log("test 123") + + text = self.mock_io.getvalue() + self.assertMessage(text, "test 123") + + def test_log_exception(self): + """Test if the SysOutCommunicatorLogger correctly logs an exception""" + logger = SysOutCommunicatorLogger.instance() + exception = Exception("something terrible happened /jk") + logger.log("test 112", exception) + + text = self.mock_io.getvalue() + self.assertMessage(text, "test 112", exception) + + def assertMessage(self, content, message, exception=None): + """Assert that the parameter message is contained in the parameter content after the date and time""" + message_pattern = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} (.*)", re.DOTALL) + + expected = message + "\n" + if exception is not None: + expected += str(exception) + "\n" + matcher = message_pattern.match(content) + + self.assertEqual(expected, matcher.group(1)) + + +if __name__ == '__main__': + unittest.main() diff --git a/worldline/__init__.py b/worldline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/__init__.py b/worldline/acquiring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/__init__.py b/worldline/acquiring/sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/api_resource.py b/worldline/acquiring/sdk/api_resource.py new file mode 100644 index 0000000..31c7ea4 --- /dev/null +++ b/worldline/acquiring/sdk/api_resource.py @@ -0,0 +1,42 @@ +from typing import Mapping, Optional + +from .communicator import Communicator + + +class ApiResource(object): + """ + Base class of all Worldline Acquiring platform API resources. + """ + + def __init__(self, parent: Optional['ApiResource'] = None, communicator: Optional[Communicator] = None, + path_context: Optional[Mapping[str, str]] = None): + """ + The parent and/or communicator must be given. + """ + if not parent and not communicator: + raise ValueError("parent and/or communicator is required") + self.__parent = parent + self.__communicator = communicator if communicator else parent._communicator + self.__path_context = path_context + + @property + def _communicator(self) -> Communicator: + return self.__communicator + + def _instantiate_uri(self, uri: str, path_context: Optional[Mapping[str, str]]) -> str: + uri = self.__replace_all(uri, path_context) + uri = self.__instantiate_uri(uri) + return uri + + def __instantiate_uri(self, uri: str) -> str: + uri = self.__replace_all(uri, self.__path_context) + if self.__parent is not None: + uri = self.__parent.__instantiate_uri(uri) + return uri + + @staticmethod + def __replace_all(uri: str, path_context: Optional[Mapping[str, str]]) -> str: + if path_context: + for key, value in path_context.items(): + uri = uri.replace("{" + key + "}", value) + return uri diff --git a/worldline/acquiring/sdk/authentication/__init__.py b/worldline/acquiring/sdk/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/authentication/authenticator.py b/worldline/acquiring/sdk/authentication/authenticator.py new file mode 100644 index 0000000..d58758a --- /dev/null +++ b/worldline/acquiring/sdk/authentication/authenticator.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import Optional, Sequence +from urllib.parse import ParseResult + +from worldline.acquiring.sdk.communication.request_header import RequestHeader + + +class Authenticator(ABC): + """ + Used to authenticate requests to the Worldline Acquiring platform. + """ + + @abstractmethod + def get_authorization(self, http_method: str, resource_uri: ParseResult, + request_headers: Optional[Sequence[RequestHeader]]) -> str: + """ + Returns a value that can be used for the "Authorization" header. + + :param http_method: The HTTP method. + :param resource_uri: The URI of the resource. + :param request_headers: A sequence of RequestHeaders. + This sequence may not be modified and may not contain headers with the same name. + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/authentication/authorization_type.py b/worldline/acquiring/sdk/authentication/authorization_type.py new file mode 100644 index 0000000..ab4180f --- /dev/null +++ b/worldline/acquiring/sdk/authentication/authorization_type.py @@ -0,0 +1,10 @@ +class AuthorizationType(object): + OAUTH2 = "OAuth2" + AUTHORIZATION_TYPES = [OAUTH2] + + @staticmethod + def get_authorization(name: str) -> str: + if name in AuthorizationType.AUTHORIZATION_TYPES: + return name + else: + raise ValueError("Authorization '{}' not found".format(name)) diff --git a/worldline/acquiring/sdk/authentication/oauth2_authenticator.py b/worldline/acquiring/sdk/authentication/oauth2_authenticator.py new file mode 100644 index 0000000..906ea2c --- /dev/null +++ b/worldline/acquiring/sdk/authentication/oauth2_authenticator.py @@ -0,0 +1,116 @@ +from datetime import datetime, timedelta +from json import loads +from threading import Lock +from typing import Iterable, Optional, Sequence, Tuple +from urllib.parse import ParseResult + +from .authenticator import Authenticator +from .oauth2_exception import OAuth2Exception + +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.communication.request_header import RequestHeader + + +class OAuth2Authenticator(Authenticator): + """ + OAuth2 Authenticator implementation. + """ + + def __init__(self, communicator_configuration: CommunicatorConfiguration): + """ + Constructs a new OAuth2Authenticator instance using the provided CommunicatorConfiguration. + + :param communicator_configuration: The configuration object containing the OAuth2 client id, client secret + and token URI, connection timeout, and socket timeout. None of these can be None or empty, + and the timeout values must be positive. + """ + Authenticator.__init__(self) + + if not communicator_configuration.oauth2_client_id: + raise ValueError("oauth2_client_id is required") + if not communicator_configuration.oauth2_client_secret: + raise ValueError("oauth2_client_secret is required") + if not communicator_configuration.oauth2_token_uri: + raise ValueError("oauth2_client_token_uri is required") + if communicator_configuration.connect_timeout <= 0: + raise ValueError("connect_timeout must be positive") + if communicator_configuration.socket_timeout <= 0: + raise ValueError("socket_timeout must be positive") + + self.__client_id = communicator_configuration.oauth2_client_id + self.__client_secret = communicator_configuration.oauth2_client_secret + self.__token_uri = communicator_configuration.oauth2_token_uri + self.__connect_timeout = communicator_configuration.connect_timeout + self.__socket_timeout = communicator_configuration.socket_timeout + self.__proxy_configuration = communicator_configuration.proxy_configuration + + # Only a limited amount of scopes may be sent in one request. + # While at the moment all scopes fit in one request, keep this code so we can easily add more token types if necessary. + # The empty path will ensure that all paths will match, as each full path ends with an empty string. + self.__access_tokens = [ + self.__TokenType("", "processing_payment", "processing_refund", "processing_credittransfer", + "processing_accountverification", + "processing_operation_reverse", "processing_dcc_rate", "services_ping"), + ] + + def get_authorization(self, http_method: Optional[str], resource_uri: Optional[ParseResult], + request_headers: Optional[Sequence[RequestHeader]]) -> str: + token_type = self.get_token_type(resource_uri.path) + with token_type.lock: + if not token_type.access_token or token_type.access_token_expiration < datetime.now(): + token_type.access_token, token_type.access_token_expiration = self.__get_access_token(token_type.scopes) + + return "Bearer " + token_type.access_token + + def __get_access_token(self, scopes: str) -> Tuple[str, datetime]: + with DefaultConnection(connect_timeout=self.__connect_timeout, + socket_timeout=self.__socket_timeout, + max_connections=1, + proxy_configuration=self.__proxy_configuration) as connection: + + request_headers = [RequestHeader("Content-Type", "application/x-www-form-urlencoded")] + body = "grant_type=client_credentials&client_id=%s&client_secret=%s&scope=%s" \ + % (self.__client_id, self.__client_secret, scopes) + + start_time = datetime.now() + + status, _, chunks = connection.post(self.__token_uri, request_headers, body) + response_body = OAuth2Authenticator.__collect_chunks(chunks) + access_token_response = loads(response_body) + + if status != 200: + error_description = access_token_response["error_description"] if "error_description" in access_token_response else None + raise OAuth2Exception("There was an error while retrieving the OAuth2 access token: %s - %s" + % (access_token_response["error"], error_description)) + + expiration_time = start_time + timedelta(seconds=access_token_response["expires_in"]) + return access_token_response["access_token"], expiration_time + + def get_token_type(self, path: str): + for token_type_entry in self.__access_tokens: + path_with_trailing_slash = token_type_entry.path + "/" + if path.endswith(token_type_entry.path) or path_with_trailing_slash in path: + return token_type_entry + + raise OAuth2Exception("Scope could not be found for path " + path) + + @staticmethod + def __collect_chunks(chunks: Iterable[bytes]) -> str: + collected_body = b"" + for chunk in chunks: + collected_body += chunk + return collected_body.decode('utf-8') + + class __TokenType: + def __init__(self, path, *scopes): + self.scopes = str.join(" ", scopes) + self.path = path + self.access_token = None + self.access_token_expiration = None + + # Python does not provide a read-write lock implementation out-of-the-box. + # Use a simple Lock instead. That does mean that multiple reads have to wait on each other, + # but the read-only part is limited to checking the access token and its expiration timestamp, + # which should take only a very short time + self.lock = Lock() diff --git a/worldline/acquiring/sdk/authentication/oauth2_exception.py b/worldline/acquiring/sdk/authentication/oauth2_exception.py new file mode 100644 index 0000000..41fcc11 --- /dev/null +++ b/worldline/acquiring/sdk/authentication/oauth2_exception.py @@ -0,0 +1,10 @@ +from typing import Optional + + +class OAuth2Exception(RuntimeError): + """ + Indicates an exception regarding the authorization with the Worldline OAuth2 Authorization Server. + """ + + def __init__(self, message: Optional[str] = None): + super(OAuth2Exception, self).__init__(message) diff --git a/worldline/acquiring/sdk/call_context.py b/worldline/acquiring/sdk/call_context.py new file mode 100644 index 0000000..44e8f33 --- /dev/null +++ b/worldline/acquiring/sdk/call_context.py @@ -0,0 +1,8 @@ +class CallContext(object): + """ + A call context can be used to send extra information with a request, and to receive extra information from a response. + + Please note that this class is not thread-safe. Each request should get its own call context instance. + """ + def __init__(self): + pass diff --git a/worldline/acquiring/sdk/client.py b/worldline/acquiring/sdk/client.py new file mode 100644 index 0000000..13fa899 --- /dev/null +++ b/worldline/acquiring/sdk/client.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import timedelta + +from .api_resource import ApiResource +from .communicator import Communicator + +from worldline.acquiring.sdk.log.body_obfuscator import BodyObfuscator +from worldline.acquiring.sdk.log.communicator_logger import CommunicatorLogger +from worldline.acquiring.sdk.log.header_obfuscator import HeaderObfuscator +from worldline.acquiring.sdk.log.logging_capable import LoggingCapable +from worldline.acquiring.sdk.log.obfuscation_capable import ObfuscationCapable +from worldline.acquiring.sdk.v1.v1_client import V1Client + + +class Client(ApiResource, LoggingCapable, ObfuscationCapable): + """ + Worldline Acquiring platform client. + + Thread-safe. + """ + + def __init__(self, communicator: Communicator): + """ + :param communicator: :class:`worldline.acquiring.sdk.communicator.Communicator` + """ + super(Client, self).__init__(communicator=communicator) + + def close_idle_connections(self, idle_time: timedelta) -> None: + """ + Utility method that delegates the call to this client's communicator. + + :param idle_time: a datetime.timedelta object indicating the idle time + """ + self._communicator.close_idle_connections(idle_time) + + def close_expired_connections(self) -> None: + """ + Utility method that delegates the call to this client's communicator. + """ + self._communicator.close_expired_connections() + + def set_body_obfuscator(self, body_obfuscator: BodyObfuscator) -> None: + # delegate to the communicator + self._communicator.set_body_obfuscator(body_obfuscator) + + def set_header_obfuscator(self, header_obfuscator: HeaderObfuscator) -> None: + # delegate to the communicator + self._communicator.set_header_obfuscator(header_obfuscator) + + def enable_logging(self, communicator_logger: CommunicatorLogger) -> None: + # delegate to the communicator + self._communicator.enable_logging(communicator_logger) + + def disable_logging(self) -> None: + # delegate to the communicator + self._communicator.disable_logging() + + def close(self) -> None: + """ + Releases any system resources associated with this object. + """ + self._communicator.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def v1(self) -> V1Client: + return V1Client(self, None) diff --git a/worldline/acquiring/sdk/communication/__init__.py b/worldline/acquiring/sdk/communication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/communication/communication_exception.py b/worldline/acquiring/sdk/communication/communication_exception.py new file mode 100644 index 0000000..f29c5e8 --- /dev/null +++ b/worldline/acquiring/sdk/communication/communication_exception.py @@ -0,0 +1,8 @@ +class CommunicationException(RuntimeError): + """ + Indicates an exception regarding the communication with the Worldline Acquiring platform such as a connection exception. + """ + + def __init__(self, exception: Exception): + super(CommunicationException, self).__init__(exception) + self.cause = exception diff --git a/worldline/acquiring/sdk/communication/connection.py b/worldline/acquiring/sdk/communication/connection.py new file mode 100644 index 0000000..66ba104 --- /dev/null +++ b/worldline/acquiring/sdk/communication/connection.py @@ -0,0 +1,84 @@ +from abc import ABC, abstractmethod +from typing import Iterable, Mapping, Sequence, Tuple, Union +from urllib.parse import ParseResult + +from .multipart_form_data_object import MultipartFormDataObject +from .request_header import RequestHeader + +from worldline.acquiring.sdk.log.logging_capable import LoggingCapable +from worldline.acquiring.sdk.log.obfuscation_capable import ObfuscationCapable + + +URI = Union[str, ParseResult] +RequestBody = Union[str, MultipartFormDataObject, None] +Response = Tuple[int, Mapping[str, str], Iterable[bytes]] + + +class Connection(LoggingCapable, ObfuscationCapable, ABC): + """ + Represents a connection to the Worldline Acquiring platform server. + """ + + @abstractmethod + def get(self, url: URI, request_headers: Sequence[RequestHeader]) -> Response: + """ + Send a GET request to the Worldline Acquiring platform and return the response. + + :param url: The URI to call, including any necessary query parameters. + :param request_headers: An optional sequence of request headers. + :return: The response from the Worldline Acquiring platform as a tuple with + the status code, headers and a generator of body chunks + :raise CommunicationException: when an exception occurred communicating + with the Worldline Acquiring platform + """ + raise NotImplementedError + + @abstractmethod + def delete(self, url: URI, request_headers: Sequence[RequestHeader]) -> Response: + """ + Send a DELETE request to the Worldline Acquiring platform and return the response. + + :param url: The URI to call, including any necessary query parameters. + :param request_headers: An optional sequence of request headers. + :return: The response from the Worldline Acquiring platform as a tuple with + the status code, headers and a generator of body chunks + :raise CommunicationException: when an exception occurred communicating + with the Worldline Acquiring platform + """ + raise NotImplementedError + + @abstractmethod + def post(self, url: URI, request_headers: Sequence[RequestHeader], body: RequestBody) -> Response: + """ + Send a POST request to the Worldline Acquiring platform and return the response. + + :param url: The URI to call, including any necessary query parameters. + :param request_headers: An optional sequence of request headers. + :param body: The optional body to send. + :return: The response from the Worldline Acquiring platform as a tuple with + the status code, headers and a generator of body chunks + :raise CommunicationException: when an exception occurred communicating + with the Worldline Acquiring platform + """ + raise NotImplementedError + + @abstractmethod + def put(self, url: URI, request_headers: Sequence[RequestHeader], body: RequestBody) -> Response: + """ + Send a PUT request to the Worldline Acquiring platform and return the response. + + :param url: The URI to call, including any necessary query parameters. + :param request_headers: An optional sequence of request headers. + :param body: The optional body to send. + :return: The response from the Worldline Acquiring platform as a tuple with + the status code, headers and a generator of body chunks + :raise CommunicationException: when an exception occurred communicating + with the Worldline Acquiring platform + """ + raise NotImplementedError + + def close(self) -> None: + """ + Releases any system resources associated with this object. + """ + pass diff --git a/worldline/acquiring/sdk/communication/default_connection.py b/worldline/acquiring/sdk/communication/default_connection.py new file mode 100644 index 0000000..42b1771 --- /dev/null +++ b/worldline/acquiring/sdk/communication/default_connection.py @@ -0,0 +1,301 @@ +import math +import uuid +from datetime import datetime, timedelta +from typing import Mapping, Optional, Sequence +from urllib.parse import urlparse + +import requests +from requests.adapters import HTTPAdapter +from requests.models import PreparedRequest +from requests.exceptions import RequestException, Timeout +from requests_toolbelt import MultipartEncoder + +from .communication_exception import CommunicationException +from .connection import RequestBody, Response, URI +from .multipart_form_data_object import MultipartFormDataObject +from .pooled_connection import PooledConnection +from .request_header import RequestHeader +from .response_header import get_header_value + +from worldline.acquiring.sdk.communicator_configuration import CommunicatorConfiguration +from worldline.acquiring.sdk.proxy_configuration import ProxyConfiguration +from worldline.acquiring.sdk.log.body_obfuscator import BodyObfuscator +from worldline.acquiring.sdk.log.communicator_logger import CommunicatorLogger +from worldline.acquiring.sdk.log.header_obfuscator import HeaderObfuscator +from worldline.acquiring.sdk.log.request_log_message import RequestLogMessage +from worldline.acquiring.sdk.log.response_log_message import ResponseLogMessage + +CHARSET = "UTF-8" + + +class DefaultConnection(PooledConnection): + """ + Provides an HTTP request interface, thread-safe + + :param connect_timeout: timeout in seconds before a pending connection is dropped + :param socket_timeout: timeout in seconds before dropping an established connection. + This is the time the server is allowed for a response + :param max_connections: the maximum number of connections in the connection pool + :param proxy_configuration: ProxyConfiguration object that contains data about proxy settings if present. + It should be writeable as string and have a scheme attribute. + + Use the methods get, delete, post and put to perform the corresponding HTTP request. + Alternatively you can use request with the request method as the first parameter. + + URI, headers and body should be given on a per-request basis. + """ + + def __init__(self, connect_timeout: int, socket_timeout: int, + max_connections: int = CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS, + proxy_configuration: Optional[ProxyConfiguration] = None): + self.logger = None + self.__requests_session = requests.session() + self.__requests_session.mount("http://", HTTPAdapter(pool_maxsize=max_connections, + pool_connections=1)) + self.__requests_session.mount("https://", HTTPAdapter(pool_maxsize=max_connections, + pool_connections=1)) + # request timeouts are in seconds + self.__connect_timeout = connect_timeout if connect_timeout >= 0 else None + self.__socket_timeout = socket_timeout if socket_timeout >= 0 else None + if proxy_configuration: + proxy = { + "http": str(proxy_configuration), + "https": str(proxy_configuration) + } + self.__requests_session.proxies = proxy + + self.__body_obfuscator = BodyObfuscator.default_body_obfuscator() + self.__header_obfuscator = HeaderObfuscator.default_header_obfuscator() + + @property + def connect_timeout(self) -> Optional[int]: + """Connection timeout in seconds""" + return self.__connect_timeout + + @property + def socket_timeout(self) -> Optional[int]: + """Socket timeout in seconds""" + return self.__socket_timeout + + def get(self, url: URI, request_headers: Sequence[RequestHeader]) -> Response: + """Perform a request to the server given by url + + :param url: the url to the server, given as a parsed url + :param request_headers: a sequence containing RequestHeader objects representing the request headers + """ + return self._request('get', url, request_headers) + + def delete(self, url: URI, request_headers: Sequence[RequestHeader]) -> Response: + """Perform a request to the server given by url + + :param url: the url to the server, given as a parsed url + :param request_headers: a sequence containing RequestHeader objects representing the request headers + """ + return self._request('delete', url, request_headers) + + def post(self, url: URI, request_headers: Sequence[RequestHeader], body: RequestBody) -> Response: + """Perform a request to the server given by url + + :param url: the url to the server, given as a parsed url + :param request_headers: a sequence containing RequestHeader objects representing the request headers + :param body: the request body + """ + if isinstance(body, MultipartFormDataObject): + body = self.__to_multipart_encoder(body) + return self._request('post', url, request_headers, body) + + def put(self, url: URI, request_headers: Sequence[RequestHeader], body: RequestBody) -> Response: + """Perform a request to the server given by url + + :param url: the url to the server, given as a parsed url + :param request_headers: a sequence containing RequestHeader objects representing the request headers + :param body: the request body + """ + if isinstance(body, MultipartFormDataObject): + body = self.__to_multipart_encoder(body) + return self._request('put', url, request_headers, body) + + @staticmethod + def __to_multipart_encoder(multipart: MultipartFormDataObject) -> MultipartEncoder: + fields = {} + for name, value in multipart.values.items(): + fields[name] = value + for name, uploadable_file in multipart.files.items(): + fields[name] = (uploadable_file.file_name, uploadable_file.content, uploadable_file.content_type) + encoder = MultipartEncoder(fields=fields, + boundary=multipart.boundary) + if encoder.content_type != multipart.content_type: + raise ValueError("MultipartEncoder did not create the expected content type") + return encoder + + class _ToResult(object): + def __call__(self, func): + def _wrapper(*args, **kwargs): + result = func(*args, **kwargs) + header = next(result) + return header + (result,) + return _wrapper + + @_ToResult() + def _request(self, method: str, url: URI, headers: Sequence[RequestHeader], body: RequestBody = None) -> Response: + """ + Perform a request to the server given by url + + :param url: the url to the server, given as a parsed url + :param headers: a sequence containing RequestHeader objects representing the request headers + :param body: the request body + """ + headers = {} if not headers else headers + if not isinstance(url, str): + url = url.geturl() + + # convert the sequence of RequestParam objects to a dictionary of key:value pairs if necessary + if headers and not isinstance(headers, dict): + headers = {param.name: param.value for param in headers} + + # send request with all parameters not declared in session and with callback for logging response + request = requests.Request(method, url, + headers=headers, + data=body, + hooks={'response': self._cb_log_response}) + prepped_request = self.__requests_session.prepare_request(request) + # add timestamp to request for later reference + prepped_request.timestamp = datetime.now() + _id = str(uuid.uuid4()) + # store random id in request so it can be matched with its response in logging + prepped_request.id = _id + self._log_request(prepped_request) + try: + timeout_ = (self.__connect_timeout, self.__socket_timeout) + requests_response = self.__requests_session.send(prepped_request, + timeout=timeout_, + stream=True) + try: + iterable = requests_response.iter_content(chunk_size=1024) + yield requests_response.status_code, requests_response.headers + for chunk in iterable: + yield chunk + finally: + requests_response.close() + + except Timeout as timeout: + self._log_error(prepped_request.id, timeout, prepped_request.timestamp) + raise CommunicationException(timeout) + except RequestException as exception: + self._log_error(prepped_request.id, exception, prepped_request.timestamp) + raise CommunicationException(exception) + except Exception as exception: + self._log_error(prepped_request.id, exception, prepped_request.timestamp) + raise + + def _log_request(self, request: PreparedRequest) -> None: + """ + Log parameter request if logging is enabled at the moment of logging. + Also adds a timestamp to the request for response logging + """ + logger = self.logger + if logger is None: + return + method = request.method + url = urlparse(request.url) + if url.query: + local_path = url.path + "?" + url.query + else: + local_path = url.path + try: + message = RequestLogMessage(request_id=request.id, + method=method, + uri=local_path, + body_obfuscator=self.__body_obfuscator, + header_obfuscator=self.__header_obfuscator) + for name in request.headers: + message.add_header(name, request.headers[name]) + body = request.body + if body: + content = request.headers['Content-Type'] + if content != "application/json": + message.set_body(body, content, CHARSET) + else: + message.set_body(body, content) + logger.log_request(message) + except Exception as exception: + logger.log("An error occurred trying to log request '{}'".format(request.id), exception) + + def _cb_log_response(self, response: requests.models.Response, **kwargs) -> None: + """Log parameter response if logging is enabled at the moment of logging""" + logger = self.logger + if logger is None: + return + request = response.request + _id = request.id + duration = math.ceil((datetime.now() - request.timestamp).total_seconds() * 1000) + status_code = response.status_code + try: + message = ResponseLogMessage(request_id=_id, + status_code=status_code, + duration=duration, + body_obfuscator=self.__body_obfuscator, + header_obfuscator=self.__header_obfuscator) + for name in response.headers: + message.add_header(name, response.headers[name]) + if self.__is_binary(response.headers): + body = "" + else: + # The response is always encoded UTF8 + # When this is not specified anywhere, the response body will be encoded in the wrong way + response.encoding = 'utf8' + body = response.text + if body: + content = response.headers['Content-Type'] + message.set_body(body, content) + logger.log_response(message) + except Exception as exception: + logger.log("An error occurred trying to log response '{}'".format(_id), exception) + + def _log_error(self, request_id: str, error: Exception, start_time: datetime) -> None: + """Log communication errors when logging is enabled""" + logger = self.logger + if logger: + duration = math.ceil((datetime.now() - start_time).total_seconds() * 1000) + logger.log("Error occurred for outgoing request (requestId='{}', {} s)".format(request_id, duration), error) + + @staticmethod + def __is_binary(headers: Mapping[str, str]) -> bool: + content_type = get_header_value(headers, "Content-Type") + if content_type is None: + return False + content_type = content_type.lower() + return not (content_type.startswith("text/") or "json" in content_type or "xml" in content_type) + + def set_body_obfuscator(self, body_obfuscator: BodyObfuscator) -> None: + self.__body_obfuscator = body_obfuscator + + def set_header_obfuscator(self, header_obfuscator: HeaderObfuscator) -> None: + self.__header_obfuscator = header_obfuscator + + def enable_logging(self, communicator_logger: CommunicatorLogger) -> None: + self.logger = communicator_logger + + def disable_logging(self) -> None: + self.logger = None + + def close_idle_connections(self, idle_time: timedelta) -> None: + """ + :param idle_time: a datetime.timedelta object indicating the idle time + """ + pass + + def close_expired_connections(self) -> None: + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self) -> None: + """ + Explicitly closes the connection + """ + self.__requests_session.close() diff --git a/worldline/acquiring/sdk/communication/metadata_provider.py b/worldline/acquiring/sdk/communication/metadata_provider.py new file mode 100644 index 0000000..59800d1 --- /dev/null +++ b/worldline/acquiring/sdk/communication/metadata_provider.py @@ -0,0 +1,131 @@ +import platform +import re +from base64 import b64encode +from typing import Optional, Sequence + +from .request_header import RequestHeader + +from worldline.acquiring.sdk.domain.data_object import DataObject +from worldline.acquiring.sdk.domain.shopping_cart_extension import ShoppingCartExtension +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller + + +class IterProperty(object): + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + return self.func(owner) + + +class MetadataProvider(object): + """ + Provides meta info about the server. + """ + __sdk_version = "0.1.0" + __server_meta_info_header = "X-WL-ServerMetaInfo" + __prohibited_headers = tuple(sorted([__server_meta_info_header, "Date", "Content-Type", "Authorization"], + key=str.lower)) + __metadata_headers: Sequence[RequestHeader] = None + + class ServerMetaInfo(DataObject): + platform_identifier = None + sdk_identifier = None + sdk_creator = None + integrator = None + shopping_cart_extension = None + + def to_dictionary(self) -> dict: + dictionary = super(MetadataProvider.ServerMetaInfo, self).to_dictionary() + if self.platform_identifier is not None: + dictionary['platformIdentifier'] = self.platform_identifier + if self.sdk_identifier is not None: + dictionary['sdkIdentifier'] = self.sdk_identifier + if self.sdk_creator is not None: + dictionary['sdkCreator'] = self.sdk_creator + if self.integrator is not None: + dictionary['integrator'] = self.integrator + if self.shopping_cart_extension is not None: + dictionary['shoppingCartExtension'] = self.shopping_cart_extension.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'MetadataProvider.ServerMetaInfo': + super(MetadataProvider.ServerMetaInfo, self).from_dictionary(dictionary) + if 'platformIdentifier' in dictionary: + self.platform_identifier = dictionary['platformIdentifier'] + if 'sdkIdentifier' in dictionary: + self.sdk_identifier = dictionary['sdkIdentifier'] + if 'sdkCreator' in dictionary: + self.sdk_creator = dictionary['sdkCreator'] + if 'integrator' in dictionary: + self.integrator = dictionary['integrator'] + if 'shoppingCartExtension' in dictionary: + if not isinstance(dictionary['shoppingCartExtension'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['shoppingCartExtension'])) + self.shopping_cart_extension = ShoppingCartExtension.create_from_dictionary(dictionary['shoppingCartExtension']) + return self + + def __init__(self, integrator: Optional[str], shopping_cart_extension: Optional[ShoppingCartExtension] = None, + additional_request_headers: Optional[Sequence[RequestHeader]] = ()): + if integrator is None or not integrator.strip(): + raise ValueError("integrator is required") + + MetadataProvider.__validate_additional_request_headers(additional_request_headers) + + def subber(name_or_value): + return re.sub(r'\r?\n(?:(?![\r\n])\s)*', " ", name_or_value).strip() + additional_request_headers = [RequestHeader(subber(header.name), subber(header.value)) for header in additional_request_headers] + + server_meta_info = self.ServerMetaInfo() + server_meta_info.platform_identifier = self._platform_identifier + server_meta_info.sdk_identifier = self._sdk_identifier + server_meta_info.sdk_creator = "Worldline" + server_meta_info.integrator = integrator + server_meta_info.shopping_cart_extension = shopping_cart_extension + + server_meta_info_string = DefaultMarshaller.instance().marshal(server_meta_info) + server_meta_info_header = RequestHeader(self.__server_meta_info_header, b64encode(server_meta_info_string.encode('utf-8')).decode('utf-8')) + if not additional_request_headers: + self.__metadata_headers = tuple([server_meta_info_header]) + else: + request_headers = [server_meta_info_header] + request_headers.extend(additional_request_headers) + self.__metadata_headers = tuple(request_headers) + + @staticmethod + def __validate_additional_request_headers(additional_request_headers: Optional[Sequence[RequestHeader]]) -> None: + if additional_request_headers is not None: + for additional_request_header in additional_request_headers: + MetadataProvider.__validate_additional_request_header(additional_request_header) + + @staticmethod + def __validate_additional_request_header(additional_request_header: RequestHeader) -> None: + try: + if additional_request_header.name in MetadataProvider.__prohibited_headers: + raise ValueError("request header not allowed: " + str(additional_request_header)) + except AttributeError: + raise AttributeError("Each request header should have an attribute 'name' and an attribute 'value'") + + @IterProperty + def prohibited_headers(self) -> Sequence[str]: + return self.__prohibited_headers + + @property + def metadata_headers(self) -> Sequence[RequestHeader]: + """ + :return: The server related headers containing the metadata to be + associated with the request (if any). This will always contain at least + an automatically generated header X-WL-ServerMetaInfo. + """ + return self.__metadata_headers + + @property + def _platform_identifier(self) -> str: + return platform.system() + " " + platform.release() + "/" + \ + platform.version() + " Python/" + platform.python_version() + \ + " (" + platform.python_implementation() + "; " + \ + str(platform.python_compiler()) + ")" + + @property + def _sdk_identifier(self) -> str: + return "PythonServerSDK/v" + self.__sdk_version diff --git a/worldline/acquiring/sdk/communication/multipart_form_data_object.py b/worldline/acquiring/sdk/communication/multipart_form_data_object.py new file mode 100644 index 0000000..cb876fb --- /dev/null +++ b/worldline/acquiring/sdk/communication/multipart_form_data_object.py @@ -0,0 +1,51 @@ +import uuid + +from typing import Mapping + +from worldline.acquiring.sdk.domain.uploadable_file import UploadableFile + + +class MultipartFormDataObject(object): + """ + A representation of a multipart/form-data object. + """ + + def __init__(self): + self.__boundary = str(uuid.uuid4()) + self.__content_type = "multipart/form-data; boundary=" + self.__boundary + self.__values = {} + self.__files = {} + + @property + def boundary(self) -> str: + return self.__boundary + + @property + def content_type(self) -> str: + return self.__content_type + + @property + def values(self) -> Mapping[str, str]: + return self.__values + + @property + def files(self) -> Mapping[str, UploadableFile]: + return self.__files + + def add_value(self, parameter_name: str, value: str) -> None: + if parameter_name is None or not parameter_name.strip(): + raise ValueError("parameter_name is required") + if value is None: + raise ValueError("value is required") + if parameter_name in self.__values or parameter_name in self.__files: + raise ValueError("duplicate parameterName: " + parameter_name) + self.__values[parameter_name] = value + + def add_file(self, parameter_name: str, uploadable_file: UploadableFile) -> None: + if parameter_name is None or not parameter_name.strip(): + raise ValueError("parameter_name is required") + if uploadable_file is None: + raise ValueError("uploadable_file is required") + if parameter_name in self.__values or parameter_name in self.__files: + raise ValueError("duplicate parameterName: " + parameter_name) + self.__files[parameter_name] = uploadable_file diff --git a/worldline/acquiring/sdk/communication/multipart_form_data_request.py b/worldline/acquiring/sdk/communication/multipart_form_data_request.py new file mode 100644 index 0000000..f76fe05 --- /dev/null +++ b/worldline/acquiring/sdk/communication/multipart_form_data_request.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +from .multipart_form_data_object import MultipartFormDataObject + + +class MultipartFormDataRequest(ABC): + """ + A representation of a multipart/form-data request. + """ + + @abstractmethod + def to_multipart_form_data_object(self) -> MultipartFormDataObject: + """ + :return: :class:`worldline.acquiring.sdk.communication.MultipartFormDataObject` + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/communication/not_found_exception.py b/worldline/acquiring/sdk/communication/not_found_exception.py new file mode 100644 index 0000000..9171ec2 --- /dev/null +++ b/worldline/acquiring/sdk/communication/not_found_exception.py @@ -0,0 +1,10 @@ +class NotFoundException(RuntimeError): + """ + Indicates an exception that occurs when the requested resource is not found. + In normal usage of the SDK, this exception should not occur, however it is possible. + For example when path parameters are set with invalid values. + """ + + def __init__(self, exception: Exception, message: str): + super(NotFoundException, self).__init__(message, exception) + self.cause = exception diff --git a/worldline/acquiring/sdk/communication/param_request.py b/worldline/acquiring/sdk/communication/param_request.py new file mode 100644 index 0000000..e229b49 --- /dev/null +++ b/worldline/acquiring/sdk/communication/param_request.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod +from typing import List + +from .request_param import RequestParam + + +class ParamRequest(ABC): + """ + Represents a set of request parameters. + """ + + @abstractmethod + def to_request_parameters(self) -> List[RequestParam]: + """ + :return: list[:class:`worldline.acquiring.sdk.communication.RequestParam`] representing the HTTP request parameters + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/communication/pooled_connection.py b/worldline/acquiring/sdk/communication/pooled_connection.py new file mode 100644 index 0000000..603cb4e --- /dev/null +++ b/worldline/acquiring/sdk/communication/pooled_connection.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod +from datetime import timedelta + +from .connection import Connection + + +class PooledConnection(Connection, ABC): + """ + Represents a pooled connection to the Worldline Acquiring platform server. + Instead of setting up a new HTTP connection for each request, this + connection uses a pool of HTTP connections. + """ + + @abstractmethod + def close_idle_connections(self, idle_time: timedelta) -> None: + """ + Closes all HTTP connections that have been idle for the specified time. + This should also include all expired HTTP connections. + + :param idle_time: a datetime.timedelta object indicating the idle time + """ + raise NotImplementedError + + @abstractmethod + def close_expired_connections(self) -> None: + """ + Closes all expired HTTP connections. + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/communication/request_header.py b/worldline/acquiring/sdk/communication/request_header.py new file mode 100644 index 0000000..590589b --- /dev/null +++ b/worldline/acquiring/sdk/communication/request_header.py @@ -0,0 +1,60 @@ +from typing import Mapping, Optional, Sequence, Union + + +class RequestHeader(object): + """ + A single request header. Immutable. + """ + + def __init__(self, name: str, value: Optional[str]): + if name is None or not name.strip(): + raise ValueError("name is required") + self.__name = name + self.__value = value + + @property + def name(self) -> str: + """ + :return: The header name. + """ + return self.__name + + @property + def value(self) -> Optional[str]: + """ + :return: The un-encoded value. + """ + return self.__value.decode('utf-8') if isinstance(self.__value, bytes) else self.__value + + def __str__(self): + return self.__name + ":" + str(self.__value) + + +def get_header_value(headers: Union[Sequence[RequestHeader], Mapping[str, str], None], header_name: str) -> Optional[str]: + """ + :return: The value of the header with the given name, or None if there was no such header. + """ + if isinstance(headers, dict): + for name, value in headers.items(): + if name.lower() == header_name.lower(): + return value + elif headers is not None: + for header in headers: + if header.name.lower() == header_name.lower(): + return header.value + return None + + +def get_header(headers: Union[Sequence[RequestHeader], Mapping[str, str], None], header_name: str) -> Optional[RequestHeader]: + """ + :return: The header with the given name, or None if there was no such header. + """ + if isinstance(headers, dict): + for name, value in headers.items(): + if name.lower() == header_name.lower(): + return RequestHeader(name, value) + elif headers is not None: + for header in headers: + if header.name.lower() == header_name.lower(): + return header + return None diff --git a/worldline/acquiring/sdk/communication/request_param.py b/worldline/acquiring/sdk/communication/request_param.py new file mode 100644 index 0000000..6f89c38 --- /dev/null +++ b/worldline/acquiring/sdk/communication/request_param.py @@ -0,0 +1,30 @@ +from typing import Optional + + +class RequestParam(object): + """ + A single request parameter. Immutable. + """ + + def __init__(self, name: str, value: Optional[str]): + if name is None or not name.strip(): + raise ValueError("name is required") + self.__name = name + self.__value = value + + @property + def name(self) -> str: + """ + :return: The parameter name. + """ + return self.__name + + @property + def value(self) -> Optional[str]: + """ + :return: The un-encoded value. + """ + return self.__value + + def __str__(self): + return self.name + ":" + self.value diff --git a/worldline/acquiring/sdk/communication/response_exception.py b/worldline/acquiring/sdk/communication/response_exception.py new file mode 100644 index 0000000..cbe35a8 --- /dev/null +++ b/worldline/acquiring/sdk/communication/response_exception.py @@ -0,0 +1,58 @@ +from typing import Mapping, Optional, Tuple + +from .response_header import get_header_value, get_header + + +class ResponseException(RuntimeError): + """ + Thrown when a response was received from the Worldline Acquiring platform which indicates an error. + """ + + def __init__(self, status: int, body: Optional[str], headers: Optional[Mapping[str, str]]): + super(ResponseException, self).__init__("the Worldline Acquiring platform returned an error response") + self.__status_code = status + self.__headers = headers if headers is not None else {} + self.__body = body + + @property + def status_code(self) -> int: + """ + :return: The HTTP status code that was returned by the Worldline Acquiring platform. + """ + return self.__status_code + + @property + def body(self) -> Optional[str]: + """ + :return: The raw response body that was returned by the Worldline Acquiring platform. + """ + return self.__body + + @property + def headers(self) -> Mapping[str, str]: + """ + :return: The headers that were returned by the Worldline Acquiring platform. Never None. + """ + return self.__headers + + def get_header(self, header_name: str) -> Optional[Tuple[str, str]]: + """ + :return: The header with the given name, or None if there was no such header. + """ + return get_header(self.__headers, header_name) + + def get_header_value(self, header_name: str) -> Optional[str]: + """ + :return: The value header with the given name, or None if there was no such header. + """ + return get_header_value(self.__headers, header_name) + + def __str__(self): + string = super(ResponseException, self).__str__() + status_code = self.__status_code + if status_code > 0: + string += "; status_code=" + str(status_code) + response_body = self.__body + if response_body: + string += "; response_body='" + response_body + "'" + return str(string) diff --git a/worldline/acquiring/sdk/communication/response_header.py b/worldline/acquiring/sdk/communication/response_header.py new file mode 100644 index 0000000..c920312 --- /dev/null +++ b/worldline/acquiring/sdk/communication/response_header.py @@ -0,0 +1,50 @@ +import re +from typing import Mapping, Optional, Tuple + + +def get_header_value(headers: Optional[Mapping[str, str]], header_name: str) -> Optional[str]: + """ + :return: The value of the header with the given name, or None if there was no such header. + """ + if headers is None: + return None + for name, value in headers.items(): + if name.lower() == header_name.lower(): + return value + return None + + +def get_header(headers: Optional[Mapping[str, str]], header_name: str) -> Optional[Tuple[str, str]]: + """ + :return: The header with the given name as a tuple with the name and value, or None if there was no such header. + """ + if headers is None: + return None + for name, value in headers.items(): + if name.lower() == header_name.lower(): + return name, value + return None + + +def get_disposition_filename(headers: Optional[Mapping[str, str]]) -> Optional[str]: + """ + :return: The value of the filename parameter of the Content-Disposition header, or None if there was no such header or parameter. + """ + header_value = get_header_value(headers, "Content-Disposition") + if header_value is None: + return None + pattern = re.compile("(?:^|;)\\s*filename\\s*=\\s*(.*?)\\s*(?:;|$)", re.IGNORECASE) + match = pattern.search(header_value) + if match is not None: + filename = match.group(1) + return __trim_quotes(filename) + return None + + +def __trim_quotes(filename: str) -> str: + if len(filename) < 2: + return filename + if (filename.startswith("\"") and filename.endswith("\"")) or \ + (filename.startswith("'") and filename.endswith("'")): + return filename[1:-1] + return filename diff --git a/worldline/acquiring/sdk/communicator.py b/worldline/acquiring/sdk/communicator.py new file mode 100644 index 0000000..d098cef --- /dev/null +++ b/worldline/acquiring/sdk/communicator.py @@ -0,0 +1,450 @@ +import uuid + +from datetime import datetime, timedelta, timezone +from typing import Any, Iterable, List, Mapping, Optional, Tuple, Type, Union +from urllib.parse import quote, urlparse, ParseResult + +from .call_context import CallContext + +from worldline.acquiring.sdk.authentication.authenticator import Authenticator +from worldline.acquiring.sdk.communication.communication_exception import CommunicationException +from worldline.acquiring.sdk.communication.connection import Connection, Response +from worldline.acquiring.sdk.communication.not_found_exception import NotFoundException +from worldline.acquiring.sdk.communication.pooled_connection import PooledConnection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.communication.multipart_form_data_object import MultipartFormDataObject +from worldline.acquiring.sdk.communication.multipart_form_data_request import MultipartFormDataRequest +from worldline.acquiring.sdk.communication.param_request import ParamRequest +from worldline.acquiring.sdk.communication.request_header import RequestHeader +from worldline.acquiring.sdk.communication.request_param import RequestParam +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.communication.response_header import get_header_value +from worldline.acquiring.sdk.json.marshaller import Marshaller, T +from worldline.acquiring.sdk.log.body_obfuscator import BodyObfuscator +from worldline.acquiring.sdk.log.communicator_logger import CommunicatorLogger +from worldline.acquiring.sdk.log.header_obfuscator import HeaderObfuscator +from worldline.acquiring.sdk.log.logging_capable import LoggingCapable +from worldline.acquiring.sdk.log.obfuscation_capable import ObfuscationCapable + + +BinaryResponse = Tuple[Mapping[str, str], Iterable[bytes]] + + +class Communicator(LoggingCapable, ObfuscationCapable): + """ + Used to communicate with the Worldline Acquiring platform web services. + + It contains all the logic to transform a request object to an HTTP request and an HTTP response to a response object. + """ + + def __init__(self, api_endpoint: Union[str, ParseResult], connection: Connection, authenticator: Authenticator, + metadata_provider: MetadataProvider, marshaller: Marshaller): + if api_endpoint is None: + raise ValueError("api_endpoint is required") + if isinstance(api_endpoint, str): + api_endpoint = urlparse(api_endpoint) + if not api_endpoint.scheme.lower() in ["http", "https"] or not api_endpoint.netloc: + raise ValueError("invalid api_endpoint: " + str(api_endpoint)) + if api_endpoint.path: + raise ValueError("api_endpoint should not contain a path") + if api_endpoint.username is not None or api_endpoint.query or api_endpoint.fragment: + raise ValueError("api_endpoint should not contain user info, query or fragment") + if connection is None: + raise ValueError("connection is required") + if authenticator is None: + raise ValueError("authenticator is required") + if metadata_provider is None: + raise ValueError("metadata_provider is required") + if marshaller is None: + raise ValueError("marshaller is required") + self.__api_endpoint = api_endpoint + self.__connection = connection + self.__authenticator = authenticator + self.__metadata_provider = metadata_provider + self.__marshaller = marshaller + + def close(self) -> None: + """ + Releases any system resources associated with this object. + """ + self.__connection.close() + + def _get_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + context: Optional[CallContext]) -> Response: + if request_parameters is None: + request_parameter_list = None + else: + request_parameter_list = request_parameters.to_request_parameters() + uri = self._to_absolute_uri(relative_path, request_parameter_list) + if request_headers is None: + request_headers = [] + self._add_generic_headers("GET", uri, request_headers, context) + + return self.__connection.get(uri, request_headers) + + def get_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + context: Optional[CallContext]) -> BinaryResponse: + """ + Corresponds to the HTTP GET method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._get_with_binary_response(relative_path, request_headers, request_parameters, context) + return self._process_binary_response(status, chunks, headers, relative_path, context) + + def get(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + response_type: Type[T], + context: Optional[CallContext]) -> T: + """ + Corresponds to the HTTP GET method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param response_type: The type of response to return. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._get_with_binary_response(relative_path, request_headers, request_parameters, context) + return self._process_response(status, chunks, headers, relative_path, response_type, context) + + def _delete_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + context: Optional[CallContext]) -> Response: + if request_parameters is None: + request_parameter_list = None + else: + request_parameter_list = request_parameters.to_request_parameters() + uri = self._to_absolute_uri(relative_path, request_parameter_list) + if request_headers is None: + request_headers = [] + self._add_generic_headers("DELETE", uri, request_headers, context) + + return self.__connection.delete(uri, request_headers) + + def delete_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + context: Optional[CallContext]) -> BinaryResponse: + """ + Corresponds to the HTTP DELETE method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._delete_with_binary_response(relative_path, request_headers, request_parameters, context) + return self._process_binary_response(status, chunks, headers, relative_path, context) + + def delete(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + response_type: Type[T], + context: Optional[CallContext]) -> T: + """ + Corresponds to the HTTP DELETE method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param response_type: The type of response to return. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._delete_with_binary_response(relative_path, request_headers, request_parameters, context) + return self._process_response(status, chunks, headers, relative_path, response_type, context) + + def _post_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], + request_body: Any, + context: Optional[CallContext]) -> Response: + if request_parameters is None: + request_parameter_list = None + else: + request_parameter_list = request_parameters.to_request_parameters() + uri = self._to_absolute_uri(relative_path, request_parameter_list) + if request_headers is None: + request_headers = [] + + body = None + if isinstance(request_body, MultipartFormDataObject): + request_headers.append(RequestHeader("Content-Type", request_body.content_type)) + body = request_body + elif isinstance(request_body, MultipartFormDataRequest): + multipart = request_body.to_multipart_form_data_object() + request_headers.append(RequestHeader("Content-Type", multipart.content_type)) + body = multipart + elif request_body is not None: + request_headers.append(RequestHeader("Content-Type", "application/json")) + body = self.__marshaller.marshal(request_body) + + self._add_generic_headers("POST", uri, request_headers, context) + return self.__connection.post(uri, request_headers, body) + + def post_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], request_body: Any, + context: Optional[CallContext]) -> BinaryResponse: + """ + Corresponds to the HTTP POST method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param request_body: The optional request body to send. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._post_with_binary_response(relative_path, request_headers, request_parameters, request_body, context) + return self._process_binary_response(status, chunks, headers, relative_path, context) + + def post(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], request_body: Any, + response_type: Type[T], + context: Optional[CallContext]) -> T: + """ + Corresponds to the HTTP POST method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param request_body: The optional request body to send. + :param response_type: The type of response to return. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._post_with_binary_response(relative_path, request_headers, request_parameters, request_body, context) + return self._process_response(status, chunks, headers, relative_path, response_type, context) + + def _put_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], request_body: Any, + context: Optional[CallContext]) -> Response: + if request_parameters is None: + request_parameter_list = None + else: + request_parameter_list = request_parameters.to_request_parameters() + uri = self._to_absolute_uri(relative_path, request_parameter_list) + if request_headers is None: + request_headers = [] + + body = None + if isinstance(request_body, MultipartFormDataObject): + request_headers.append(RequestHeader("Content-Type", request_body.content_type)) + body = request_body + elif isinstance(request_body, MultipartFormDataRequest): + multipart = request_body.to_multipart_form_data_object() + request_headers.append(RequestHeader("Content-Type", multipart.content_type)) + body = multipart + elif request_body is not None: + request_headers.append(RequestHeader("Content-Type", "application/json")) + body = self.__marshaller.marshal(request_body) + + self._add_generic_headers("PUT", uri, request_headers, context) + return self.__connection.put(uri, request_headers, body) + + def put_with_binary_response(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], request_body: Any, + context: Optional[CallContext]) -> BinaryResponse: + """ + Corresponds to the HTTP PUT method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param request_body: The optional request body to send. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._put_with_binary_response(relative_path, request_headers, request_parameters, request_body, context) + return self._process_binary_response(status, chunks, headers, relative_path, context) + + def put(self, relative_path: str, request_headers: Optional[List[RequestHeader]], + request_parameters: Optional[ParamRequest], request_body: Any, + response_type: Type[T], + context: Optional[CallContext]) -> T: + """ + Corresponds to the HTTP PUT method. + + :param relative_path: The path to call, relative to the base URI. + :param request_headers: An optional list of request headers. + :param request_parameters: An optional set of request parameters. + :param request_body: The optional request body to send. + :param response_type: The type of response to return. + :param context: The optional call context to use. + :raise CommunicationException: when an exception occurred communicating with the Worldline Acquiring platform + :raise ResponseException: when an error response was received from the Worldline Acquiring platform + """ + (status, headers, chunks) = self._put_with_binary_response(relative_path, request_headers, request_parameters, request_body, context) + return self._process_response(status, chunks, headers, relative_path, response_type, context) + + @property + def marshaller(self) -> Marshaller: + """ + :return: The Marshaller object associated with this communicator. Never None. + """ + return self.__marshaller + + def _to_absolute_uri(self, relative_path: str, request_parameters: List[RequestParam]) -> ParseResult: + if relative_path.startswith("/"): + absolute_path = relative_path + else: + absolute_path = "/" + relative_path + uri = self.__api_endpoint.geturl() + absolute_path + separator = "?" + if request_parameters is not None: + for nvp in request_parameters: + uri += separator + uri += quote(nvp.name) + "=" + quote(nvp.value) + separator = "&" + # no need to revalidate that uri has a valid scheme and netloc + return urlparse(uri) + + def _add_generic_headers(self, http_method: str, uri: ParseResult, request_headers: List[RequestHeader], + context: Optional[CallContext]) -> None: + """ + Adds the necessary headers to the given list of headers. This includes + the authorization header, which uses other headers, so when you need to + override this method, make sure to call super.addGenericHeaders at the + end of your overridden method. + """ + # add server meta info header + request_headers.extend(self.__metadata_provider.metadata_headers) + # add date header + request_headers.append(RequestHeader("Date", self._get_header_date_string())) + # add trace id header + request_headers.append(RequestHeader("Trace-Id", str(uuid.uuid4()))) + # no context specific headers at this time + # add authorization + authenticator = self.__authenticator + authorization = authenticator.get_authorization(http_method, uri, request_headers) + request_headers.append(RequestHeader("Authorization", authorization)) + + @staticmethod + def _get_header_date_string() -> str: + """ + Returns the date in the preferred format for the HTTP date header. + """ + date_format_utc = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT") + return date_format_utc + + @staticmethod + def __collect_chunks(chunks: Iterable[bytes]) -> str: + collected_body = b"" + for chunk in chunks: + collected_body += chunk + return collected_body.decode('utf-8') + + def _process_binary_response(self, status: int, chunks: Iterable[bytes], headers: Mapping[str, str], request_path: str, + context: Optional[CallContext]) -> BinaryResponse: + if context is not None: + self._update_context(headers, context) + self._throw_exception_if_necessary_binary(status, chunks, headers, request_path) + return headers, chunks + + def _process_response(self, status: int, chunks: Iterable[bytes], headers: Mapping[str, str], request_path: str, + response_type: Type[T], + context: Optional[CallContext]) -> T: + if context is not None: + self._update_context(headers, context) + body = self.__collect_chunks(chunks) + self._throw_exception_if_necessary(status, body, headers, request_path) + return self.__marshaller.unmarshal(body, response_type) + + @staticmethod + def _update_context(headers: Mapping[str, str], context: Optional[CallContext]) -> None: + """ + Updates the given call context based on the contents of the given response. + """ + # no context specific headers at this time + pass + + def _throw_exception_if_necessary_binary(self, status: int, chunks: Iterable[bytes], headers: Mapping[str, str], request_path: str) -> None: + """ + Checks the Response for errors and throws an exception if necessary. + """ + # status codes in the 100 or 300 range are not expected + if status < 200 or status >= 300: + body = self.__collect_chunks(chunks) + self.__throw_exception(status, body, headers, request_path) + + def _throw_exception_if_necessary(self, status: int, body: str, headers: Mapping[str, str], request_path: str) -> None: + """ + Checks the Response for errors and throws an exception if necessary. + """ + # status codes in the 100 or 300 range are not expected + if status < 200 or status >= 300: + self.__throw_exception(status, body, headers, request_path) + + def __throw_exception(self, status: int, body: str, headers: Mapping[str, str], request_path: str) -> None: + if body is not None and not self.__is_json(headers): + cause = ResponseException(status, body, headers) + if status == 404: + raise NotFoundException(cause, "The requested resource was not found; invalid path: " + request_path) + else: + raise CommunicationException(cause) + else: + raise ResponseException(status, body, headers) + + @staticmethod + def __is_json(headers: Mapping[str, str]) -> bool: + content_type = get_header_value(headers, "Content-Type") + if content_type is None: + return True + content_type = content_type.lower() + return ("application/json" == content_type) or \ + ("application/problem+json" == content_type) or \ + (content_type.startswith("application/json")) or \ + (content_type.startswith("application/problem+json")) + + def close_idle_connections(self, idle_time: timedelta) -> None: + """ + Utility method that delegates the call to this communicator's connection + if that's an instance of PooledConnection. If not this method does nothing. + + :param idle_time: a datetime.timedelta object indicating the idle time + """ + if isinstance(self.__connection, PooledConnection): + self.__connection.close_idle_connections(idle_time) + + def close_expired_connections(self) -> None: + """ + Utility method that delegates the call to this communicator's connection + if that's an instance of PooledConnection. If not this method does nothing. + """ + if isinstance(self.__connection, PooledConnection): + self.__connection.close_expired_connections() + + def set_body_obfuscator(self, body_obfuscator: BodyObfuscator) -> None: + # delegate to the connection + self.__connection.set_body_obfuscator(body_obfuscator) + + def set_header_obfuscator(self, header_obfuscator: HeaderObfuscator) -> None: + # delegate to the connection + self.__connection.set_header_obfuscator(header_obfuscator) + + def enable_logging(self, communicator_logger: CommunicatorLogger) -> None: + # delegate to the connection + self.__connection.enable_logging(communicator_logger) + + def disable_logging(self) -> None: + # delegate to the connection + self.__connection.disable_logging() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() diff --git a/worldline/acquiring/sdk/communicator_configuration.py b/worldline/acquiring/sdk/communicator_configuration.py new file mode 100644 index 0000000..c50d9fc --- /dev/null +++ b/worldline/acquiring/sdk/communicator_configuration.py @@ -0,0 +1,308 @@ +from configparser import ConfigParser, NoOptionError +from typing import Optional, Union +from urllib.parse import urlparse, ParseResult + +from .proxy_configuration import ProxyConfiguration + +from worldline.acquiring.sdk.authentication.authorization_type import AuthorizationType +from worldline.acquiring.sdk.domain.shopping_cart_extension import ShoppingCartExtension + + +# pylint: disable=too-many-instance-attributes +# Necessary to load information from config +class CommunicatorConfiguration(object): + """ + Configuration for the communicator. + """ + # The default number of maximum connections. + DEFAULT_MAX_CONNECTIONS = 10 + + __api_endpoint: Optional[ParseResult] = None + __connect_timeout: Optional[int] = None + __socket_timeout: Optional[int] = None + __max_connections: Optional[int] = None + __authorization_type: Optional[str] = None + __authorization_id: Optional[str] = None + __authorization_secret: Optional[str] = None + __oauth2_token_uri: Optional[str] = None + __proxy_configuration: Optional[ProxyConfiguration] = None + __integrator: Optional[str] = None + __shopping_cart_extension: Optional[ShoppingCartExtension] = None + + def __init__(self, properties: Optional[ConfigParser] = None, + api_endpoint: Union[str, ParseResult, None] = None, + authorization_id: Optional[str] = None, authorization_secret: Optional[str] = None, + oauth2_client_id: Optional[str] = None, oauth2_client_secret: Optional[str] = None, + oauth2_token_uri: Optional[str] = None, + authorization_type: Optional[str] = None, + connect_timeout: Optional[int] = None, socket_timeout: Optional[int] = None, + max_connections: Optional[int] = None, proxy_configuration: Optional[ProxyConfiguration] = None, + integrator: Optional[str] = None, shopping_cart_extension: Optional[ShoppingCartExtension] = None): + """ + :param properties: a ConfigParser.ConfigParser object containing configuration data + :param connect_timeout: connection timeout for the network communication in seconds + :param socket_timeout: socket timeout for the network communication in seconds + :param max_connections: The maximum number of connections in the connection pool + """ + if properties and properties.sections() and properties.options("AcquiringSDK"): + self.__api_endpoint = self.__get_endpoint(properties) + authorization = properties.get("AcquiringSDK", "acquiring.api.authorizationType") + self.__authorization_type = AuthorizationType.get_authorization(authorization) + self.__connect_timeout = int(properties.get("AcquiringSDK", "acquiring.api.connectTimeout")) + self.__socket_timeout = int(properties.get("AcquiringSDK", "acquiring.api.socketTimeout")) + self.__max_connections = self.__get_property(properties, "acquiring.api.maxConnections", + self.DEFAULT_MAX_CONNECTIONS) + try: + self.oauth2_token_uri = properties.get("AcquiringSDK", "acquiring.api.oauth2.tokenUri") + except NoOptionError: + self.oauth2_token_uri = None + try: + proxy_uri = properties.get("AcquiringSDK", "acquiring.api.proxy.uri") + except NoOptionError: + proxy_uri = None + try: + proxy_user = properties.get("AcquiringSDK", "acquiring.api.proxy.username") + except NoOptionError: + proxy_user = None + try: + proxy_pass = properties.get("AcquiringSDK", "acquiring.api.proxy.password") + except NoOptionError: + proxy_pass = None + if proxy_uri is not None: + self.__proxy_configuration = ProxyConfiguration.from_uri(proxy_uri, proxy_user, proxy_pass) + else: + self.__proxy_configuration = None + try: + self.__integrator = properties.get("AcquiringSDK", "acquiring.api.integrator") + except NoOptionError: + self.__integrator = None + try: + self.__shopping_cart_extension = self.__get_shopping_cart_extension(properties) + except NoOptionError: + self.__shopping_cart_extension = None + + if api_endpoint: + self.api_endpoint = api_endpoint + if authorization_id: + self.authorization_id = authorization_id + if authorization_secret: + self.authorization_secret = authorization_secret + if oauth2_client_id: + self.oauth2_client_id = oauth2_client_id + if oauth2_client_secret: + self.oauth2_client_secret = oauth2_client_secret + if oauth2_token_uri: + self.oauth2_token_uri = oauth2_token_uri + if authorization_type: + self.authorization_type = authorization_type + if connect_timeout: + self.connect_timeout = connect_timeout + if socket_timeout: + self.socket_timeout = socket_timeout + if max_connections: + self.max_connections = max_connections + if proxy_configuration: + self.proxy_configuration = proxy_configuration + if integrator: + self.integrator = integrator + if shopping_cart_extension: + self.shopping_cart_extension = shopping_cart_extension + + @staticmethod + def __get_property(properties: ConfigParser, key: str, default_value: int) -> int: + try: + property_value = properties.get("AcquiringSDK", key) + except NoOptionError: + property_value = None + if property_value is not None: + return int(property_value) + else: + return default_value + + def __get_endpoint(self, properties: ConfigParser) -> ParseResult: + host = properties.get("AcquiringSDK", "acquiring.api.endpoint.host") + try: + scheme = properties.get("AcquiringSDK", "acquiring.api.endpoint.scheme") + except NoOptionError: + scheme = None + try: + port = properties.get("AcquiringSDK", "acquiring.api.endpoint.port") + except NoOptionError: + port = None + if scheme: + if port: + return self.__create_uri(scheme, host, int(port)) + else: + return self.__create_uri(scheme, host, -1) + elif port: + return self.__create_uri("https", host, int(port)) + else: + return self.__create_uri("https", host, -1) + + @staticmethod + def __create_uri(scheme: str, host: str, port: int) -> ParseResult: + if port != -1: + uri = scheme + "://" + host + ":" + str(port) + else: + uri = scheme + "://" + host + url = urlparse(uri) + if not url.scheme.lower() in ["http", "https"] or not url.netloc: + raise ValueError("Unable to construct endpoint URI") + return url + + @staticmethod + def __get_shopping_cart_extension(properties: ConfigParser) -> Optional[ShoppingCartExtension]: + try: + creator = properties.get("AcquiringSDK", "acquiring.api.shoppingCartExtension.creator") + except NoOptionError: + creator = None + try: + name = properties.get("AcquiringSDK", "acquiring.api.shoppingCartExtension.name") + except NoOptionError: + name = None + try: + version = properties.get("AcquiringSDK", "acquiring.api.shoppingCartExtension.version") + except NoOptionError: + version = None + try: + extension_id = properties.get("AcquiringSDK", "acquiring.api.shoppingCartExtension.extensionId") + except NoOptionError: + extension_id = None + if creator is None and name is None and version is None and extension_id is None: + return None + else: + return ShoppingCartExtension(creator, name, version, extension_id) + + @property + def api_endpoint(self) -> Optional[ParseResult]: + """ + The Worldline Acquiring platform API endpoint URI. + """ + return self.__api_endpoint + + @api_endpoint.setter + def api_endpoint(self, api_endpoint: Union[str, ParseResult, None]) -> None: + if isinstance(api_endpoint, str): + api_endpoint = urlparse(str(api_endpoint)) + if api_endpoint is not None and api_endpoint.path: + raise ValueError("apiEndpoint should not contain a path") + if api_endpoint is not None and \ + (api_endpoint.username is not None or api_endpoint.query or api_endpoint.fragment): + raise ValueError("apiEndpoint should not contain user info, query or fragment") + self.__api_endpoint = api_endpoint + + @property + def authorization_id(self) -> Optional[str]: + """ + An id used for authorization. The meaning of this id is different for each authorization type. + For instance, for OAuth2 this is the client id. + """ + return self.__authorization_id + + @authorization_id.setter + def authorization_id(self, authorization_id: Optional[str]) -> None: + self.__authorization_id = authorization_id + + @property + def authorization_secret(self) -> Optional[str]: + """ + A secret used for authorization. The meaning of this secret is different for each authorization type. + For instance, for OAuth2 this is the client secret. + """ + return self.__authorization_secret + + @authorization_secret.setter + def authorization_secret(self, authorization_secret: Optional[str]) -> None: + self.__authorization_secret = authorization_secret + + @ property + def oauth2_client_id(self) -> Optional[str]: + """ + The OAuth2 client id. + + This property is an alias for authorization_id + """ + return self.authorization_id + + @ oauth2_client_id.setter + def oauth2_client_id(self, oauth2_client_id: Optional[str]) -> None: + self.authorization_id = oauth2_client_id + + @ property + def oauth2_client_secret(self) -> Optional[str]: + """ + The OAuth2 client secret. + + This property is an alias for authorization_secret + """ + return self.__authorization_secret + + @ oauth2_client_secret.setter + def oauth2_client_secret(self, oauth2_client_secret: Optional[str]) -> None: + self.authorization_secret = oauth2_client_secret + + @ property + def oauth2_token_uri(self) -> Optional[str]: + return self.__oauth2_token_uri + + @ oauth2_token_uri.setter + def oauth2_token_uri(self, oauth2_token_uri: Optional[str]) -> None: + self.__oauth2_token_uri = oauth2_token_uri + + @property + def authorization_type(self) -> Optional[str]: + return self.__authorization_type + + @authorization_type.setter + def authorization_type(self, authorization_type: Optional[str]) -> None: + self.__authorization_type = authorization_type + + @property + def connect_timeout(self) -> Optional[int]: + """Connection timeout for the underlying network communication in seconds""" + return self.__connect_timeout + + @connect_timeout.setter + def connect_timeout(self, connect_timeout: Optional[int]) -> None: + self.__connect_timeout = connect_timeout + + @property + def socket_timeout(self) -> Optional[int]: + """Socket timeout for the underlying network communication in seconds""" + return self.__socket_timeout + + @socket_timeout.setter + def socket_timeout(self, socket_timeout: Optional[int]) -> None: + self.__socket_timeout = socket_timeout + + @property + def max_connections(self) -> Optional[int]: + return self.__max_connections + + @max_connections.setter + def max_connections(self, max_connections: Optional[int]) -> None: + self.__max_connections = max_connections + + @property + def proxy_configuration(self) -> Optional[ProxyConfiguration]: + return self.__proxy_configuration + + @proxy_configuration.setter + def proxy_configuration(self, proxy_configuration: Optional[ProxyConfiguration]) -> None: + self.__proxy_configuration = proxy_configuration + + @property + def integrator(self) -> Optional[str]: + return self.__integrator + + @integrator.setter + def integrator(self, integrator: Optional[str]) -> None: + self.__integrator = integrator + + @property + def shopping_cart_extension(self) -> Optional[ShoppingCartExtension]: + return self.__shopping_cart_extension + + @shopping_cart_extension.setter + def shopping_cart_extension(self, shopping_cart_extension: Optional[ShoppingCartExtension]) -> None: + self.__shopping_cart_extension = shopping_cart_extension diff --git a/worldline/acquiring/sdk/domain/__init__.py b/worldline/acquiring/sdk/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/domain/data_object.py b/worldline/acquiring/sdk/domain/data_object.py new file mode 100644 index 0000000..a75ffb4 --- /dev/null +++ b/worldline/acquiring/sdk/domain/data_object.py @@ -0,0 +1,34 @@ +from datetime import date, datetime, timezone + + +class DataObject(object): + def to_dictionary(self) -> dict: + return {} + + def from_dictionary(self, dictionary: dict) -> 'DataObject': + if not isinstance(dictionary, dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary)) + return self + + @staticmethod + def parse_date(s: str) -> date: + return date.fromisoformat(s) + + @staticmethod + def format_date(d: date) -> str: + return d.isoformat() + + @staticmethod + def parse_datetime(s: str) -> datetime: + if s.endswith('Z'): + s = s[:-1] + '+00:00' + return datetime.fromisoformat(s) + + @staticmethod + def format_datetime(dt: datetime) -> str: + # isoformat(timespec='milliseconds') works fine, as long as there is a timezone + # if there isn't one, add the current timezone + if dt.tzinfo is None: + zone = datetime.fromtimestamp(dt.timestamp(), timezone.utc).astimezone().tzinfo + dt = datetime.fromtimestamp(dt.timestamp(), zone) + return dt.isoformat(timespec='milliseconds') diff --git a/worldline/acquiring/sdk/domain/shopping_cart_extension.py b/worldline/acquiring/sdk/domain/shopping_cart_extension.py new file mode 100644 index 0000000..8260bbd --- /dev/null +++ b/worldline/acquiring/sdk/domain/shopping_cart_extension.py @@ -0,0 +1,77 @@ +from typing import Optional + +from .data_object import DataObject + + +class ShoppingCartExtension(DataObject): + def __init__(self, creator: str, name: str, version: str, extension_id: Optional[str] = None): + if not creator: + raise ValueError("creator is required") + if not name: + raise ValueError("name is required") + if not version: + raise ValueError("version is required") + self.__creator = creator + self.__name = name + self.__version = version + self.__extension_id = extension_id + + def to_dictionary(self) -> dict: + dictionary = super(ShoppingCartExtension, self).to_dictionary() + if self.__creator is not None: + dictionary['creator'] = self.__creator + if self.__name is not None: + dictionary['name'] = self.__name + if self.__version is not None: + dictionary['version'] = self.__version + if self.__extension_id is not None: + dictionary['extensionId'] = self.__extension_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ShoppingCartExtension': + super(ShoppingCartExtension, self).from_dictionary(dictionary) + if 'creator' in dictionary: + self.__creator = dictionary['creator'] + if 'name' in dictionary: + self.__name = dictionary['name'] + if 'version' in dictionary: + self.__version = dictionary['version'] + if 'extensionId' in dictionary: + self.__extension_id = dictionary['extensionId'] + return self + + @staticmethod + def create_from_dictionary(dictionary: dict) -> 'ShoppingCartExtension': + if 'creator' in dictionary: + creator = dictionary['creator'] + else: + raise ValueError("creator is required") + if 'name' in dictionary: + name = dictionary['name'] + else: + raise ValueError("name is required") + if 'version' in dictionary: + version = dictionary['version'] + else: + raise ValueError("version is required") + if 'extensionId' in dictionary: + extension_id = dictionary['extensionId'] + else: + extension_id = None + return ShoppingCartExtension(creator, name, version, extension_id) + + @property + def creator(self) -> str: + return self.__creator + + @property + def name(self) -> str: + return self.__name + + @property + def version(self) -> str: + return self.__version + + @property + def extension_id(self) -> Optional[str]: + return self.__extension_id diff --git a/worldline/acquiring/sdk/domain/uploadable_file.py b/worldline/acquiring/sdk/domain/uploadable_file.py new file mode 100644 index 0000000..eaec2ac --- /dev/null +++ b/worldline/acquiring/sdk/domain/uploadable_file.py @@ -0,0 +1,51 @@ +from typing import Any + + +class UploadableFile(object): + """ + A file that can be uploaded. + + The allowed forms of content are defined by the Connection implementation. + The default implementation supports strings, file descriptors and io.BytesIO objects. + """ + + def __init__(self, file_name: str, content: Any, content_type: str, content_length: int = -1): + if file_name is None or not file_name.strip(): + raise ValueError("file_name is required") + if content is None: + raise ValueError("content is required") + if content_type is None or not content_type.strip(): + raise ValueError("file_name is required") + + self.__file_name = file_name + self.__content = content + self.__content_type = content_type + self.__content_length = max(content_length, -1) + + @property + def file_name(self) -> str: + """ + :return: The name of the file. + """ + return self.__file_name + + @property + def content(self) -> Any: + """ + :return: The file's content. + """ + return self.__content + + @property + def content_type(self) -> str: + """ + :return: The file's content type. + """ + return self.__content_type + + @property + def content_length(self) -> int: + """ + :return: The file's content length, or -1 if not known. + """ + return self.__content_length diff --git a/worldline/acquiring/sdk/factory.py b/worldline/acquiring/sdk/factory.py new file mode 100644 index 0000000..12ae6f5 --- /dev/null +++ b/worldline/acquiring/sdk/factory.py @@ -0,0 +1,124 @@ +from configparser import ConfigParser +from typing import Optional, Union + +from .client import Client +from .communicator import Communicator +from .communicator_configuration import CommunicatorConfiguration + +from worldline.acquiring.sdk.authentication.authorization_type import AuthorizationType +from worldline.acquiring.sdk.authentication.authenticator import Authenticator +from worldline.acquiring.sdk.authentication.oauth2_authenticator import OAuth2Authenticator +from worldline.acquiring.sdk.communication.connection import Connection +from worldline.acquiring.sdk.communication.default_connection import DefaultConnection +from worldline.acquiring.sdk.communication.metadata_provider import MetadataProvider +from worldline.acquiring.sdk.json.default_marshaller import DefaultMarshaller +from worldline.acquiring.sdk.json.marshaller import Marshaller + + +class Factory(object): + """ + Worldline Acquiring platform factory for several SDK components. + """ + + @staticmethod + def create_configuration(configuration_file_name: Union[str, bytes], authorization_id: str, authorization_secret: str) -> CommunicatorConfiguration: + """ + Creates a CommunicatorConfiguration based on the configuration values in configuration_file_name, authorization_id and authorization_secret. + """ + try: + parser = ConfigParser() + with open(configuration_file_name) as f: + parser.read_file(f) + return CommunicatorConfiguration(properties=parser, + authorization_id=authorization_id, + authorization_secret=authorization_secret) + except IOError as e: + raise RuntimeError("Unable to read configuration located at {}".format(e.filename), e) from e + + @staticmethod + def create_communicator_from_configuration(communicator_configuration: CommunicatorConfiguration, + metadata_provider: Optional[MetadataProvider] = None, + connection: Optional[Connection] = None, + authenticator: Optional[Authenticator] = None, + marshaller: Optional[Marshaller] = None) -> Communicator: + """ + Creates a Communicator based on the configuration stored in the CommunicatorConfiguration argument + """ + if metadata_provider is None: + metadata_provider = MetadataProvider(integrator=communicator_configuration.integrator, + shopping_cart_extension=communicator_configuration.shopping_cart_extension) + if connection is None: + connection = DefaultConnection(communicator_configuration.connect_timeout, + communicator_configuration.socket_timeout, + communicator_configuration.max_connections, + communicator_configuration.proxy_configuration) + if authenticator is None: + authenticator = Factory.__get_authenticator(communicator_configuration) + if marshaller is None: + marshaller = DefaultMarshaller.instance() + return Communicator(api_endpoint=communicator_configuration.api_endpoint, + metadata_provider=metadata_provider, + connection=connection, + authenticator=authenticator, + marshaller=marshaller) + + @staticmethod + def __get_authenticator(communicator_configuration: CommunicatorConfiguration) -> Authenticator: + if communicator_configuration.authorization_type == AuthorizationType.OAUTH2: + return OAuth2Authenticator(communicator_configuration) + raise RuntimeError("Unknown authorizationType " + communicator_configuration.authorization_type) + + @staticmethod + def create_communicator_from_file(configuration_file_name: Union[str, bytes], authorization_id: str, authorization_secret: str, + metadata_provider: Optional[MetadataProvider] = None, + connection: Optional[Connection] = None, + authenticator: Optional[Authenticator] = None, + marshaller: Optional[Marshaller] = None) -> Communicator: + """ + Creates a Communicator based on the configuration values in configuration_file_name, api_id_key and authorization_secret. + """ + configuration = Factory.create_configuration(configuration_file_name, authorization_id, authorization_secret) + return Factory.create_communicator_from_configuration(configuration, + metadata_provider=metadata_provider, + connection=connection, + authenticator=authenticator, + marshaller=marshaller) + + @staticmethod + def create_client_from_configuration(communicator_configuration: CommunicatorConfiguration, + metadata_provider: Optional[MetadataProvider] = None, + connection: Optional[Connection] = None, + authenticator: Optional[Authenticator] = None, + marshaller: Optional[Marshaller] = None) -> Client: + """ + Create a Client based on the configuration stored in the CommunicatorConfiguration argument + """ + communicator = Factory.create_communicator_from_configuration(communicator_configuration, + metadata_provider=metadata_provider, + connection=connection, + authenticator=authenticator, + marshaller=marshaller) + return Client(communicator) + + @staticmethod + def create_client_from_communicator(communicator: Communicator) -> Client: + """ + Create a Client based on the settings stored in the Communicator argument + """ + return Client(communicator) + + @staticmethod + def create_client_from_file(configuration_file_name: Union[str, bytes], authorization_id: str, authorization_secret: str, + metadata_provider: Optional[MetadataProvider] = None, + connection: Optional[Connection] = None, + authenticator: Optional[Authenticator] = None, + marshaller: Optional[Marshaller] = None) -> Client: + """ + Creates a Client based on the configuration values in configuration_file_name, authorization_id and authorization_secret. + """ + communicator = Factory.create_communicator_from_file(configuration_file_name, authorization_id, authorization_secret, + metadata_provider=metadata_provider, + connection=connection, + authenticator=authenticator, + marshaller=marshaller) + return Client(communicator) diff --git a/worldline/acquiring/sdk/json/__init__.py b/worldline/acquiring/sdk/json/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/json/default_marshaller.py b/worldline/acquiring/sdk/json/default_marshaller.py new file mode 100644 index 0000000..4483bf2 --- /dev/null +++ b/worldline/acquiring/sdk/json/default_marshaller.py @@ -0,0 +1,60 @@ +from json import dumps, loads +from typing import Any, Type, Union + +from .marshaller import Marshaller, T +from .marshaller_syntax_exception import MarshallerSyntaxException + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class DefaultMarshaller(Marshaller): + """ + Marshaller implementation based on json. + """ + + @staticmethod + def instance() -> 'DefaultMarshaller': + return _DEFAULT_MARSHALLER_INSTANCE + + def marshal(self, request_object: Any) -> str: + if isinstance(request_object, DataObject): + dictionary = request_object.to_dictionary() + return dumps(dictionary, + default=lambda o: o.to_dictionary(), + indent=4) + else: + return dumps(request_object, + default=lambda o: o.__dict__, + indent=4) + + def unmarshal(self, response_json: Union[str, bytes, None], type_class: Type[T]) -> T: + if not response_json: + return None + if issubclass(type_class, DataObject): + try: + return type_class().from_dictionary(loads(response_json)) + except ValueError as e: + raise MarshallerSyntaxException(e) from e + + class Object(object): + pass + + def convert_to_object(d): + try: + d = dict(d) + except (TypeError, ValueError): + return d + o = Object() + for key, value in d.items(): + o.__dict__[key] = convert_to_object(value) + return o + + try: + dictionary = loads(response_json) + converted_object = convert_to_object(dictionary) + return converted_object + except ValueError as e: + raise MarshallerSyntaxException(e) from e + + +_DEFAULT_MARSHALLER_INSTANCE = DefaultMarshaller() diff --git a/worldline/acquiring/sdk/json/marshaller.py b/worldline/acquiring/sdk/json/marshaller.py new file mode 100644 index 0000000..e8dfcd7 --- /dev/null +++ b/worldline/acquiring/sdk/json/marshaller.py @@ -0,0 +1,32 @@ +from abc import ABC, abstractmethod +from typing import Any, Optional, Type, TypeVar, Union + + +T = TypeVar('T') + + +class Marshaller(ABC): + """ + Used to marshal and unmarshal Worldline Acquiring platform request and response objects to and from JSON. + """ + + @abstractmethod + def marshal(self, request_object: Any) -> str: + """ + Marshal a request object to a JSON string. + + :param request_object: the object to marshal into a serialized JSON string + :return: the serialized JSON string of the request_object + """ + raise NotImplementedError + + @abstractmethod + def unmarshal(self, response_json: Union[str, bytes, None], type_class: Type[T]) -> Optional[T]: + """ + Unmarshal a JSON string to a response object. + + :param response_json: the json body that should be unmarshalled + :param type_class: The class to which the response_json should be unmarshalled + :raise MarshallerSyntaxException: if the JSON is not a valid representation for an object of the given type + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/json/marshaller_syntax_exception.py b/worldline/acquiring/sdk/json/marshaller_syntax_exception.py new file mode 100644 index 0000000..66a455a --- /dev/null +++ b/worldline/acquiring/sdk/json/marshaller_syntax_exception.py @@ -0,0 +1,13 @@ +from typing import Optional + + +class MarshallerSyntaxException(RuntimeError): + """ + Thrown when a JSON string cannot be converted to a response object. + """ + + def __init__(self, cause: Optional[Exception] = None): + if cause: + super(MarshallerSyntaxException, self).__init__(cause) + else: + super(MarshallerSyntaxException, self).__init__() diff --git a/worldline/acquiring/sdk/log/__init__.py b/worldline/acquiring/sdk/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worldline/acquiring/sdk/log/body_obfuscator.py b/worldline/acquiring/sdk/log/body_obfuscator.py new file mode 100644 index 0000000..d315d14 --- /dev/null +++ b/worldline/acquiring/sdk/log/body_obfuscator.py @@ -0,0 +1,100 @@ +import codecs +import re + +from typing import AnyStr, Dict, Mapping, Optional, Pattern, Sequence, Union + +from .obfuscation_rule import ObfuscationRule, obfuscate_all, obfuscate_all_but_first, obfuscate_all_but_last + + +class BodyObfuscator(object): + """ + A class that can be used to obfuscate properties in JSON bodies. + """ + + __obfuscation_rules: Dict[str, ObfuscationRule] = None + __property_pattern: Pattern[AnyStr] = None + + def __init__(self, additional_rules: Optional[Mapping[str, ObfuscationRule]] = None): + """ + Creates a new body obfuscator. + This will contain some pre-defined obfuscation rules, as well as any provided custom rules + + :param additional_rules: An optional mapping from property names to obfuscation rules, + where an obfuscation rule is a function that obfuscates a single string, + """ + self.__obfuscation_rules = { + "address": obfuscate_all(), + "authenticationValue": obfuscate_all_but_first(4), + "bin": obfuscate_all_but_first(6), + "cardholderAddress": obfuscate_all(), + "cardholderPostalCode": obfuscate_all(), + "cardNumber": obfuscate_all_but_last(4), + "cardSecurityCode": obfuscate_all(), + "city": obfuscate_all(), + "cryptogram": obfuscate_all_but_first(4), + "expiryDate": obfuscate_all_but_last(4), + "name": obfuscate_all(), + "paymentAccountReference": obfuscate_all_but_first(6), + "postalCode": obfuscate_all(), + "stateCode": obfuscate_all(), + + } + if additional_rules: + for name, rule in additional_rules.items(): + self.__obfuscation_rules[name] = rule + + property_names = tuple(self.__obfuscation_rules.keys()) + self.__property_pattern = self.__build_property_pattern(property_names) + + @staticmethod + def __build_property_pattern(property_names: Sequence[str]) -> Pattern[AnyStr]: + if not property_names: + return re.compile("$^") + s = "([\"'])(" + for p in property_names: + s += '|' + re.escape(p) + s += ")\\1\\s*:\\s*(?:([\"'])(.*?)(? str: + obfuscation_rule = self.__obfuscation_rules.get(property_name) + if obfuscation_rule: + return obfuscation_rule(value) + return value + + def obfuscate_body(self, body: Union[str, bytes, None], charset: Optional[str] = None) -> Optional[str]: + """ + Obfuscates the body from the given stream as necessary. + :param body: The body to obfuscate, as string or bytes. + :param charset: The charset to use to read the body bytes. + """ + if charset: + body = codecs.decode(body, charset) + + if body is None: + return None + if not body: + return "" + index = 0 + s_obfuscate = "" + matcher = self.__property_pattern.finditer(body) + for x in matcher: + property_name = x.group(2) + value = x.group(4) + value_start = x.start(4) + value_end = x.end(4) + if not value: + value = x.group(5) + value_start = x.start(5) + value_end = x.end(5) + obfuscated_value = self.__obfuscate_value(property_name, value) + s_obfuscate += body[index:value_start] + obfuscated_value + index = value_end + return s_obfuscate + body[index:] + + @staticmethod + def default_body_obfuscator() -> 'BodyObfuscator': + return _DEFAULT_BODY_OBFUSCATOR + + +_DEFAULT_BODY_OBFUSCATOR = BodyObfuscator() diff --git a/worldline/acquiring/sdk/log/communicator_logger.py b/worldline/acquiring/sdk/log/communicator_logger.py new file mode 100644 index 0000000..4a52748 --- /dev/null +++ b/worldline/acquiring/sdk/log/communicator_logger.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from .request_log_message import RequestLogMessage +from .response_log_message import ResponseLogMessage + + +class CommunicatorLogger(ABC): + """ + Used to log messages from communicators. + """ + + def log_request(self, request_log_message: RequestLogMessage) -> None: + """ + Logs a request message object + + """ + self.log(request_log_message.get_message()) + + def log_response(self, response_log_message: ResponseLogMessage) -> None: + """ + Logs a response message object + + """ + self.log(response_log_message.get_message()) + + @abstractmethod + def log(self, message: str, thrown: Optional[Exception] = None) -> None: + """ + Logs a throwable with an accompanying message. + + :param message: The message accompanying the throwable. + :param thrown: The throwable to log. + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/log/header_obfuscator.py b/worldline/acquiring/sdk/log/header_obfuscator.py new file mode 100644 index 0000000..ec03324 --- /dev/null +++ b/worldline/acquiring/sdk/log/header_obfuscator.py @@ -0,0 +1,47 @@ +from typing import Dict, Mapping, Optional + +from .obfuscation_rule import ObfuscationRule, obfuscate_with_fixed_length + + +class HeaderObfuscator(object): + """ + A class that can be used to obfuscate headers. + """ + + __obfuscation_rules: Dict[str, ObfuscationRule] = None + + def __init__(self, additional_rules: Optional[Mapping[str, ObfuscationRule]] = None): + """ + Creates a new header obfuscator. + This will contain some pre-defined obfuscation rules, as well as any provided custom rules + + :param additional_rules: An optional mapping from property names to obfuscation rules, + where an obfuscation rule is a function that obfuscates a single string, + """ + self.__obfuscation_rules = { + "authorization": obfuscate_with_fixed_length(8), + "www-authenticate": obfuscate_with_fixed_length(8), + "proxy-authenticate": obfuscate_with_fixed_length(8), + "proxy-authorization": obfuscate_with_fixed_length(8), + } + if additional_rules: + for name, rule in additional_rules.items(): + name = name.lower() + self.__obfuscation_rules[name] = rule + + def obfuscate_header(self, header_name: str, value: str) -> str: + """ + Obfuscates the value for the given header as necessary. + """ + header_name = header_name.lower() + obfuscation_rule = self.__obfuscation_rules.get(header_name) + if obfuscation_rule: + return obfuscation_rule(value) + return value + + @staticmethod + def default_header_obfuscator() -> 'HeaderObfuscator': + return _DEFAULT_HEADER_OBFUSCATOR + + +_DEFAULT_HEADER_OBFUSCATOR = HeaderObfuscator() diff --git a/worldline/acquiring/sdk/log/log_message.py b/worldline/acquiring/sdk/log/log_message.py new file mode 100644 index 0000000..2dd0407 --- /dev/null +++ b/worldline/acquiring/sdk/log/log_message.py @@ -0,0 +1,94 @@ +from abc import ABC, abstractmethod +from typing import List, Optional, Sequence, Tuple, Union + +from .body_obfuscator import BodyObfuscator +from .header_obfuscator import HeaderObfuscator + + +class LogMessage(ABC): + """ + A utility class to build log messages. + """ + __request_id: str = None + __headers: str = None + __body: Optional[str] = None + __content_type: Optional[str] = None + __header_list: List[Tuple[str, str]] = None + __body_obfuscator: BodyObfuscator = None + __header_obfuscator: HeaderObfuscator = None + + def __init__(self, request_id: str, + body_obfuscator: BodyObfuscator = BodyObfuscator.default_body_obfuscator(), + header_obfuscator: HeaderObfuscator = HeaderObfuscator.default_header_obfuscator()): + if not request_id: + raise ValueError("request_id is required") + if not body_obfuscator: + raise ValueError("body_obfuscator is required") + if not header_obfuscator: + raise ValueError("header_obfuscator is required") + self.__request_id = request_id + self.__headers = "" + self.__header_list = [] + self.__body_obfuscator = body_obfuscator + self.__header_obfuscator = header_obfuscator + + @property + def request_id(self) -> str: + return self.__request_id + + @property + def headers(self) -> str: + return str(self.__headers) + + @property + def body(self) -> Optional[str]: + return self.__body + + @property + def content_type(self) -> Optional[str]: + return self.__content_type + + def add_header(self, name: str, value: Optional[str]) -> None: + if self.__headers: + self.__headers += ", " + self.__headers += name + "=\"" + if value is not None and value.lower() != 'none': + obfuscated_value = self.__header_obfuscator.obfuscate_header(name, value) + self.__headers += obfuscated_value + self.__header_list.append((name, "\"" + obfuscated_value + "\"")) + else: + self.__header_list.append((name, "\"\"")) + self.__headers += "\"" + + def set_body(self, body: Union[str, bytes, None], content_type: Optional[str], charset: Optional[str] = None) -> None: + self.__content_type = content_type + if self.__is_binary(content_type): + self.__body = "" + else: + self.__body = self.__body_obfuscator.obfuscate_body(body, charset) + + def set_binary_body(self, content_type: Optional[str]) -> None: + if not self.__is_binary(content_type): + raise ValueError("Not a binary content type: " + content_type) + self.__content_type = content_type + self.__body = "" + + @staticmethod + def __is_binary(content_type: Optional[str]) -> bool: + if content_type is None: + return False + content_type = content_type.lower() + return not (content_type.startswith("text/") or "json" in content_type or "xml" in content_type) + + @staticmethod + def empty_if_none(value: Optional[str]) -> str: + if value is not None: + return value + return "" + + @abstractmethod + def get_message(self) -> str: + raise NotImplementedError + + def get_header_list(self) -> Sequence[Tuple[str, str]]: + return self.__header_list diff --git a/worldline/acquiring/sdk/log/logging_capable.py b/worldline/acquiring/sdk/log/logging_capable.py new file mode 100644 index 0000000..56be826 --- /dev/null +++ b/worldline/acquiring/sdk/log/logging_capable.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + +from .communicator_logger import CommunicatorLogger + + +class LoggingCapable(ABC): + """ + Classes that extend this class have support for log messages from + communicators. + """ + + @abstractmethod + def enable_logging(self, communicator_logger: CommunicatorLogger) -> None: + """ + Turns on logging using the given communicator logger. + + :raise ValueError: If the given communicator logger is None. + """ + raise NotImplementedError + + @abstractmethod + def disable_logging(self) -> None: + """ + Turns off logging. + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/log/obfuscation_capable.py b/worldline/acquiring/sdk/log/obfuscation_capable.py new file mode 100644 index 0000000..a7fde14 --- /dev/null +++ b/worldline/acquiring/sdk/log/obfuscation_capable.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod + +from .body_obfuscator import BodyObfuscator +from .header_obfuscator import HeaderObfuscator + + +class ObfuscationCapable(ABC): + """ + Classes that extend this class support obfuscating bodies and headers. + """ + + @abstractmethod + def set_body_obfuscator(self, body_obfuscator: BodyObfuscator) -> None: + """ + Sets the current body obfuscator to use. + """ + raise NotImplementedError + + @abstractmethod + def set_header_obfuscator(self, header_obfuscator: HeaderObfuscator) -> None: + """ + Sets the current header obfuscator to use. + """ + raise NotImplementedError diff --git a/worldline/acquiring/sdk/log/obfuscation_rule.py b/worldline/acquiring/sdk/log/obfuscation_rule.py new file mode 100644 index 0000000..8e34c3d --- /dev/null +++ b/worldline/acquiring/sdk/log/obfuscation_rule.py @@ -0,0 +1,50 @@ +from typing import Callable + + +ObfuscationRule = Callable[[str], str] + + +def obfuscate_all() -> ObfuscationRule: + """ + Returns an obfuscation rule (function) that will replace all characters with * + """ + return lambda value: '*' * len(value) if value else value + + +def obfuscate_with_fixed_length(fixed_length: int) -> ObfuscationRule: + """ + Returns an obfuscation rule (function) that will replace values with a fixed length string containing only * + """ + return lambda value: '*' * fixed_length + + +def obfuscate_all_but_first(count: int) -> ObfuscationRule: + """ + Returns an obfuscation rule (function) that will keep a fixed number of characters at the start, + then replaces all other characters with * + """ + def obfuscate_value(value: str) -> str: + if not value: + return value + length = len(value) + if length < count: + return value + end = '*' * (length - count) + return value[:count] + end + return obfuscate_value + + +def obfuscate_all_but_last(count: int) -> ObfuscationRule: + """ + Returns an obfuscation rule that will keep a fixed number of characters at the end, + then replaces all other characters with * + """ + def obfuscate_value(value: str) -> str: + if not value: + return value + length = len(value) + if length < count: + return value + start = '*' * (length - count) + return start + value[-count:] + return obfuscate_value diff --git a/worldline/acquiring/sdk/log/python_communicator_logger.py b/worldline/acquiring/sdk/log/python_communicator_logger.py new file mode 100644 index 0000000..e9e7289 --- /dev/null +++ b/worldline/acquiring/sdk/log/python_communicator_logger.py @@ -0,0 +1,54 @@ +from logging import Logger +from typing import Optional + +from .communicator_logger import CommunicatorLogger + + +class PythonCommunicatorLogger(CommunicatorLogger): + """ + A communicator logger that is backed by the log library. + """ + + def __init__(self, logger: Logger, log_level: int, error_log_level: Optional[int] = None): + """ + Logs messages to the argument logger using the argument log_level. + If absent, the error_log_level will be equal to the log_level. + Note that if the CommunicatorLogger's log level is lower than the + argument logger's log level (e.g. the CommunicatorLogger is given + log.INFO as level and the argument logger has a level of + log.WARNING), then nothing will be logged to the logger. + + :param logger: the logger to log to + :param log_level: the log level that will be used for non-error + messages logged via the CommunicatorLogger + :param error_log_level: the log level that will be used for error + messages logged via the CommunicatorLogger. + """ + CommunicatorLogger.__init__(self) + if not error_log_level: + error_log_level = log_level + if logger is None: + raise ValueError("logger is required") + if log_level is None: + raise ValueError("log_level is required") + if error_log_level is None: + raise ValueError("error_log_level is required") + self.__logger = logger + self.__log_level = log_level + self.__error_log_level = error_log_level + + def log(self, message: str, thrown: Optional[Exception] = None) -> None: + """ + Log a message to the underlying logger. + If thrown is absent, the message will be logged with the + CommunicatorLogger's log_level, if a thrown object is provided, + the message and exception will be logged with the CommunicatorLogger's + error_log_level. + + :param message: the message to be logged + :param thrown: an optional throwable object + """ + if not thrown: + self.__logger.log(self.__log_level, message) + else: + self.__logger.log(self.__error_log_level, message, thrown) diff --git a/worldline/acquiring/sdk/log/request_log_message.py b/worldline/acquiring/sdk/log/request_log_message.py new file mode 100644 index 0000000..9eb5fad --- /dev/null +++ b/worldline/acquiring/sdk/log/request_log_message.py @@ -0,0 +1,28 @@ +from .body_obfuscator import BodyObfuscator +from .header_obfuscator import HeaderObfuscator +from .log_message import LogMessage + + +class RequestLogMessage(LogMessage): + """ + A utility class to build request log messages. + """ + + def __init__(self, request_id: str, method: str, uri: str, + body_obfuscator: BodyObfuscator = BodyObfuscator.default_body_obfuscator(), + header_obfuscator: HeaderObfuscator = HeaderObfuscator.default_header_obfuscator()): + super(RequestLogMessage, self).__init__(request_id, body_obfuscator, header_obfuscator) + self.method = method + self.uri = uri + + def get_message(self) -> str: + string = "Outgoing request (requestId='" + str(self.request_id) + "'):\n" + \ + " method: " + self.empty_if_none("'" + self.method + "'") + "\n" + \ + " uri: " + self.empty_if_none("'" + self.uri + "'") + "\n" + \ + " headers: " + self.headers + body = self.body + if body is None: + return string + else: + return string + "\n content-type: '" + self.empty_if_none(self.content_type) + "'\n" + \ + " body: '" + body + "'" diff --git a/worldline/acquiring/sdk/log/response_log_message.py b/worldline/acquiring/sdk/log/response_log_message.py new file mode 100644 index 0000000..39748da --- /dev/null +++ b/worldline/acquiring/sdk/log/response_log_message.py @@ -0,0 +1,37 @@ +from .body_obfuscator import BodyObfuscator +from .header_obfuscator import HeaderObfuscator +from .log_message import LogMessage + + +class ResponseLogMessage(LogMessage): + """ + A utility class to build request log messages. + """ + + def __init__(self, request_id: str, status_code: int, duration: int = -1, + body_obfuscator: BodyObfuscator = BodyObfuscator.default_body_obfuscator(), + header_obfuscator: HeaderObfuscator = HeaderObfuscator.default_header_obfuscator()): + super(ResponseLogMessage, self).__init__(request_id, body_obfuscator, header_obfuscator) + self.__status_code = status_code + self.__duration = duration + + def get_duration(self) -> int: + return self.__duration + + def get_status_code(self) -> int: + return self.__status_code + + def get_message(self) -> str: + if self.__duration < 0: + return "Incoming response (requestId='" + self.request_id + "'):\n" + \ + " status_code: " + str(self.__status_code) + "\n" + \ + " headers: " + self.headers + "\n" + \ + " content-type: " + self.empty_if_none(self.content_type) + "\n" + \ + " body: " + self.empty_if_none(self.body) + + else: + return "Incoming response (requestId='" + self.request_id + "', " + str(self.__duration) + " ms):\n" + \ + " status_code: " + str(self.__status_code) + "\n" + \ + " headers: " + self.headers + "\n" + \ + " content-type: " + self.empty_if_none(self.content_type) + "\n" + \ + " body: " + self.empty_if_none(self.body) diff --git a/worldline/acquiring/sdk/log/sys_out_communicator_logger.py b/worldline/acquiring/sdk/log/sys_out_communicator_logger.py new file mode 100644 index 0000000..780e28d --- /dev/null +++ b/worldline/acquiring/sdk/log/sys_out_communicator_logger.py @@ -0,0 +1,36 @@ +from datetime import datetime +from threading import Lock +from typing import Optional + +from .communicator_logger import CommunicatorLogger + + +class SysOutCommunicatorLogger(CommunicatorLogger): + """ + A communicator logger that prints its message to sys.stdout + It includes a timestamp in yyyy-MM-ddTHH:mm:ss format in the system time zone. + """ + + _global_lock = Lock() + _old_print = print + + @staticmethod + def instance() -> 'SysOutCommunicatorLogger': + return _SYS_OUT_COMMUNICATOR_LOGGER_INSTANCE + + def __print(self, *a) -> None: + with self._global_lock: + self._old_print(*a) + + def log(self, message: str, thrown: Optional[Exception] = None) -> None: + # Make sure the same object is used for locking and printing + self.__print(self.__get_date_prefix() + message) + if thrown: + self.__print(str(thrown)) + + @staticmethod + def __get_date_prefix() -> str: + return datetime.now().strftime("%Y-%m-%dT%H:%M:%S ") + + +_SYS_OUT_COMMUNICATOR_LOGGER_INSTANCE = SysOutCommunicatorLogger() diff --git a/worldline/acquiring/sdk/proxy_configuration.py b/worldline/acquiring/sdk/proxy_configuration.py new file mode 100644 index 0000000..940e908 --- /dev/null +++ b/worldline/acquiring/sdk/proxy_configuration.py @@ -0,0 +1,102 @@ +from typing import Optional +from urllib.parse import urlparse + + +class ProxyConfiguration(object): + """ + HTTP proxy configuration. + + Can be initialised directly from a host and port or can be constructed from a uri using fromuri + """ + + def __init__(self, host: str, port: int, scheme: str = "http", username: Optional[str] = None, password: Optional[str] = None): + if not host: + raise ValueError("host is required") + if not port: + raise ValueError("port is required") + if port <= 0 or port > 65535: + raise ValueError("""port "{}" is invalid""".format(port)) + self.__scheme = scheme + self.__host = host + self.__port = port + self.__username = username + self.__password = password + + @staticmethod + def from_uri(uri: str, username: Optional[str] = None, password: Optional[str] = None) -> 'ProxyConfiguration': + """ + Constructs a ProxyConfiguration from a URI; if username and/or password + are given they will be used instead of corresponding data in the URI + """ + parsed = urlparse(uri) + scheme = parsed.scheme + host = parsed.hostname + parsed.path + port = parsed.port + if username is None: + username = parsed.username + if password is None: + password = parsed.password + if port is None: + if scheme == "http": + port = 80 + elif scheme == "https": + port = 443 + else: + raise ValueError("unsupported scheme: " + scheme) + return ProxyConfiguration(scheme=scheme, + host=host, + port=port, + username=username, + password=password) + + @property + def scheme(self) -> str: + return self.__scheme + + @scheme.setter + def scheme(self, scheme: str) -> None: + self.__scheme = scheme + + @property + def host(self) -> str: + return self.__host + + @host.setter + def host(self, host: str) -> None: + self.__host = host + + @property + def port(self) -> int: + return self.__port + + @port.setter + def port(self, port: int) -> None: + self.__port = port + + @property + def username(self) -> Optional[str]: + return self.__username + + @username.setter + def username(self, username: Optional[str]) -> None: + self.__username = username + + @property + def password(self) -> Optional[str]: + return self.__password + + @password.setter + def password(self, password: Optional[str]) -> None: + self.__password = password + + def __str__(self): + """ + Return a proxy string in the form scheme://username:password@host:port + or scheme://host:port if authentication is absent + + Supports HTTP Basic Auth + """ + if self.username is None or self.password is None: + return r"{0}://{1}:{2}".format(self.scheme, self.host, self.port) + else: + return r"{0}://{3}:{4}@{1}:{2}".format(self.scheme, self.host, self.port, self.username, self.password) diff --git a/worldline/acquiring/sdk/v1/__init__.py b/worldline/acquiring/sdk/v1/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/__init__.py b/worldline/acquiring/sdk/v1/acquirer/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/acquirer_client.py b/worldline/acquiring/sdk/v1/acquirer/acquirer_client.py new file mode 100644 index 0000000..dfe089a --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/acquirer_client.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.v1.acquirer.merchant.merchant_client import MerchantClient + + +class AcquirerClient(ApiResource): + """ + Acquirer client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(AcquirerClient, self).__init__(parent=parent, path_context=path_context) + + def merchant(self, merchant_id: str) -> MerchantClient: + """ + Resource /processing/v1/{acquirerId}/{merchantId} + + :param merchant_id: str + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.merchant_client.MerchantClient` + """ + sub_context = { + "merchantId": merchant_id, + } + return MerchantClient(self, sub_context) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.py new file mode 100644 index 0000000..2058f6d --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_account_verification_request import ApiAccountVerificationRequest +from worldline.acquiring.sdk.v1.domain.api_account_verification_response import ApiAccountVerificationResponse +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class AccountVerificationsClient(ApiResource): + """ + AccountVerifications client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(AccountVerificationsClient, self).__init__(parent=parent, path_context=path_context) + + def process_account_verification(self, body: ApiAccountVerificationRequest, context: Optional[CallContext] = None) -> ApiAccountVerificationResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/account-verifications - Verify account + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Account-Verifications/operation/processAccountVerification + + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_account_verification_request.ApiAccountVerificationRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_account_verification_response.ApiAccountVerificationResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/account-verifications", None) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiAccountVerificationResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.py new file mode 100644 index 0000000..cdaba47 --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.domain.get_dcc_rate_request import GetDCCRateRequest +from worldline.acquiring.sdk.v1.domain.get_dcc_rate_response import GetDccRateResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class DynamicCurrencyConversionClient(ApiResource): + """ + DynamicCurrencyConversion client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(DynamicCurrencyConversionClient, self).__init__(parent=parent, path_context=path_context) + + def request_dcc_rate(self, body: GetDCCRateRequest, context: Optional[CallContext] = None) -> GetDccRateResponse: + """ + Resource /services/v1/{acquirerId}/{merchantId}/dcc-rates - Request DCC rate + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Dynamic-Currency-Conversion/operation/requestDccRate + + :param body: :class:`worldline.acquiring.sdk.v1.domain.get_dcc_rate_request.GetDCCRateRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.get_dcc_rate_response.GetDccRateResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + uri = self._instantiate_uri("/services/v1/{acquirerId}/{merchantId}/dcc-rates", None) + try: + return self._communicator.post( + uri, + None, + None, + body, + GetDccRateResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.py new file mode 100644 index 0000000..10fdf7a --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.v1.acquirer.merchant.accountverifications.account_verifications_client import AccountVerificationsClient +from worldline.acquiring.sdk.v1.acquirer.merchant.dynamiccurrencyconversion.dynamic_currency_conversion_client import DynamicCurrencyConversionClient +from worldline.acquiring.sdk.v1.acquirer.merchant.payments.payments_client import PaymentsClient +from worldline.acquiring.sdk.v1.acquirer.merchant.refunds.refunds_client import RefundsClient +from worldline.acquiring.sdk.v1.acquirer.merchant.technicalreversals.technical_reversals_client import TechnicalReversalsClient + + +class MerchantClient(ApiResource): + """ + Merchant client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(MerchantClient, self).__init__(parent=parent, path_context=path_context) + + def payments(self) -> PaymentsClient: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments + + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.payments.payments_client.PaymentsClient` + """ + return PaymentsClient(self, None) + + def refunds(self) -> RefundsClient: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/refunds + + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.refunds.refunds_client.RefundsClient` + """ + return RefundsClient(self, None) + + def account_verifications(self) -> AccountVerificationsClient: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/account-verifications + + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.accountverifications.account_verifications_client.AccountVerificationsClient` + """ + return AccountVerificationsClient(self, None) + + def technical_reversals(self) -> TechnicalReversalsClient: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/operations/{operationId}/reverse + + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.technicalreversals.technical_reversals_client.TechnicalReversalsClient` + """ + return TechnicalReversalsClient(self, None) + + def dynamic_currency_conversion(self) -> DynamicCurrencyConversionClient: + """ + Resource /services/v1/{acquirerId}/{merchantId}/dcc-rates + + :return: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.dynamiccurrencyconversion.dynamic_currency_conversion_client.DynamicCurrencyConversionClient` + """ + return DynamicCurrencyConversionClient(self, None) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/payments/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.py b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.py new file mode 100644 index 0000000..4ebd7af --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import List, Optional + +from worldline.acquiring.sdk.communication.param_request import ParamRequest +from worldline.acquiring.sdk.communication.request_param import RequestParam + + +class GetPaymentStatusParams(ParamRequest): + """ + Query parameters for Retrieve payment + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/getPaymentStatus + """ + + __return_operations: Optional[bool] = None + + @property + def return_operations(self) -> Optional[bool]: + """ + | If true, the response will contain the operations of the payment. False by default. + + Type: bool + """ + return self.__return_operations + + @return_operations.setter + def return_operations(self, value: Optional[bool]) -> None: + self.__return_operations = value + + def to_request_parameters(self) -> List[RequestParam]: + """ + :return: list[RequestParam] + """ + result = [] + if self.return_operations is not None: + result.append(RequestParam("returnOperations", str(self.return_operations))) + return result diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.py new file mode 100644 index 0000000..dbe1780 --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from .get_payment_status_params import GetPaymentStatusParams + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_action_response import ApiActionResponse +from worldline.acquiring.sdk.v1.domain.api_action_response_for_refund import ApiActionResponseForRefund +from worldline.acquiring.sdk.v1.domain.api_capture_request import ApiCaptureRequest +from worldline.acquiring.sdk.v1.domain.api_increment_request import ApiIncrementRequest +from worldline.acquiring.sdk.v1.domain.api_increment_response import ApiIncrementResponse +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.domain.api_payment_refund_request import ApiPaymentRefundRequest +from worldline.acquiring.sdk.v1.domain.api_payment_request import ApiPaymentRequest +from worldline.acquiring.sdk.v1.domain.api_payment_resource import ApiPaymentResource +from worldline.acquiring.sdk.v1.domain.api_payment_response import ApiPaymentResponse +from worldline.acquiring.sdk.v1.domain.api_payment_reversal_request import ApiPaymentReversalRequest +from worldline.acquiring.sdk.v1.domain.api_reversal_response import ApiReversalResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class PaymentsClient(ApiResource): + """ + Payments client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(PaymentsClient, self).__init__(parent=parent, path_context=path_context) + + def process_payment(self, body: ApiPaymentRequest, context: Optional[CallContext] = None) -> ApiPaymentResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments - Create payment + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/processPayment + + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_payment_request.ApiPaymentRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_payment_response.ApiPaymentResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments", None) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiPaymentResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def get_payment_status(self, payment_id: str, query: GetPaymentStatusParams, context: Optional[CallContext] = None) -> ApiPaymentResource: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments/{paymentId} - Retrieve payment + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/getPaymentStatus + + :param payment_id: str + :param query: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.payments.get_payment_status_params.GetPaymentStatusParams` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_payment_resource.ApiPaymentResource` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "paymentId": payment_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}", path_context) + try: + return self._communicator.get( + uri, + None, + query, + ApiPaymentResource, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def simple_capture_of_payment(self, payment_id: str, body: ApiCaptureRequest, context: Optional[CallContext] = None) -> ApiActionResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/captures - Capture payment + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/simpleCaptureOfPayment + + :param payment_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_capture_request.ApiCaptureRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_action_response.ApiActionResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "paymentId": payment_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/captures", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiActionResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def reverse_authorization(self, payment_id: str, body: ApiPaymentReversalRequest, context: Optional[CallContext] = None) -> ApiReversalResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/authorization-reversals - Reverse authorization + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/reverseAuthorization + + :param payment_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_payment_reversal_request.ApiPaymentReversalRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_reversal_response.ApiReversalResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "paymentId": payment_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/authorization-reversals", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiReversalResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def increment_payment(self, payment_id: str, body: ApiIncrementRequest, context: Optional[CallContext] = None) -> ApiIncrementResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/increments - Increment authorization + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/incrementPayment + + :param payment_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_increment_request.ApiIncrementRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_increment_response.ApiIncrementResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "paymentId": payment_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/increments", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiIncrementResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def create_refund(self, payment_id: str, body: ApiPaymentRefundRequest, context: Optional[CallContext] = None) -> ApiActionResponseForRefund: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/refunds - Refund payment + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Payments/operation/createRefund + + :param payment_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_payment_refund_request.ApiPaymentRefundRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_action_response_for_refund.ApiActionResponseForRefund` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "paymentId": payment_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/payments/{paymentId}/refunds", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiActionResponseForRefund, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.py b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.py new file mode 100644 index 0000000..c6d65d0 --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import List, Optional + +from worldline.acquiring.sdk.communication.param_request import ParamRequest +from worldline.acquiring.sdk.communication.request_param import RequestParam + + +class GetRefundParams(ParamRequest): + """ + Query parameters for Retrieve refund + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Refunds/operation/getRefund + """ + + __return_operations: Optional[bool] = None + + @property + def return_operations(self) -> Optional[bool]: + """ + | If true, the response will contain the operations of the payment. False by default. + + Type: bool + """ + return self.__return_operations + + @return_operations.setter + def return_operations(self, value: Optional[bool]) -> None: + self.__return_operations = value + + def to_request_parameters(self) -> List[RequestParam]: + """ + :return: list[RequestParam] + """ + result = [] + if self.return_operations is not None: + result.append(RequestParam("returnOperations", str(self.return_operations))) + return result diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.py new file mode 100644 index 0000000..24984d2 --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from .get_refund_params import GetRefundParams + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_action_response_for_refund import ApiActionResponseForRefund +from worldline.acquiring.sdk.v1.domain.api_capture_request_for_refund import ApiCaptureRequestForRefund +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.domain.api_payment_reversal_request import ApiPaymentReversalRequest +from worldline.acquiring.sdk.v1.domain.api_refund_request import ApiRefundRequest +from worldline.acquiring.sdk.v1.domain.api_refund_resource import ApiRefundResource +from worldline.acquiring.sdk.v1.domain.api_refund_response import ApiRefundResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class RefundsClient(ApiResource): + """ + Refunds client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(RefundsClient, self).__init__(parent=parent, path_context=path_context) + + def process_standalone_refund(self, body: ApiRefundRequest, context: Optional[CallContext] = None) -> ApiRefundResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/refunds - Create standalone refund + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Refunds/operation/processStandaloneRefund + + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_refund_request.ApiRefundRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_refund_response.ApiRefundResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/refunds", None) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiRefundResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def get_refund(self, refund_id: str, query: GetRefundParams, context: Optional[CallContext] = None) -> ApiRefundResource: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/refunds/{refundId} - Retrieve refund + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Refunds/operation/getRefund + + :param refund_id: str + :param query: :class:`worldline.acquiring.sdk.v1.acquirer.merchant.refunds.get_refund_params.GetRefundParams` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_refund_resource.ApiRefundResource` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "refundId": refund_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/refunds/{refundId}", path_context) + try: + return self._communicator.get( + uri, + None, + query, + ApiRefundResource, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def capture_refund(self, refund_id: str, body: ApiCaptureRequestForRefund, context: Optional[CallContext] = None) -> ApiActionResponseForRefund: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/refunds/{refundId}/captures - Capture refund + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Refunds/operation/captureRefund + + :param refund_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_capture_request_for_refund.ApiCaptureRequestForRefund` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_action_response_for_refund.ApiActionResponseForRefund` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "refundId": refund_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/refunds/{refundId}/captures", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiActionResponseForRefund, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) + + def reverse_refund_authorization(self, refund_id: str, body: ApiPaymentReversalRequest, context: Optional[CallContext] = None) -> ApiActionResponseForRefund: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/refunds/{refundId}/authorization-reversals - Reverse refund authorization + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Refunds/operation/reverseRefundAuthorization + + :param refund_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_payment_reversal_request.ApiPaymentReversalRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_action_response_for_refund.ApiActionResponseForRefund` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "refundId": refund_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/refunds/{refundId}/authorization-reversals", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiActionResponseForRefund, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/__init__.py b/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.py b/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.py new file mode 100644 index 0000000..37c4a71 --- /dev/null +++ b/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.domain.api_technical_reversal_request import ApiTechnicalReversalRequest +from worldline.acquiring.sdk.v1.domain.api_technical_reversal_response import ApiTechnicalReversalResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class TechnicalReversalsClient(ApiResource): + """ + TechnicalReversals client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(TechnicalReversalsClient, self).__init__(parent=parent, path_context=path_context) + + def technical_reversal(self, operation_id: str, body: ApiTechnicalReversalRequest, context: Optional[CallContext] = None) -> ApiTechnicalReversalResponse: + """ + Resource /processing/v1/{acquirerId}/{merchantId}/operations/{operationId}/reverse - Technical reversal + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Technical-Reversals/operation/technicalReversal + + :param operation_id: str + :param body: :class:`worldline.acquiring.sdk.v1.domain.api_technical_reversal_request.ApiTechnicalReversalRequest` + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: :class:`worldline.acquiring.sdk.v1.domain.api_technical_reversal_response.ApiTechnicalReversalResponse` + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + path_context = { + "operationId": operation_id, + } + uri = self._instantiate_uri("/processing/v1/{acquirerId}/{merchantId}/operations/{operationId}/reverse", path_context) + try: + return self._communicator.post( + uri, + None, + None, + body, + ApiTechnicalReversalResponse, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/api_exception.py b/worldline/acquiring/sdk/v1/api_exception.py new file mode 100644 index 0000000..0f9affc --- /dev/null +++ b/worldline/acquiring/sdk/v1/api_exception.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + + +class ApiException(RuntimeError): + """ + Represents an error response from the Worldline Acquiring platform. + """ + + def __init__(self, status_code: int, response_body: str, type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str], + message: str = "The Worldline Acquiring platform returned an error response"): + super(ApiException, self).__init__(message) + self.__status_code = status_code + self.__response_body = response_body + self.__type = type + self.__title = title + self.__status = status + self.__detail = detail + self.__instance = instance + + @property + def status_code(self) -> int: + """ + :return: The HTTP status code that was returned by the Worldline Acquiring platform. + """ + return self.__status_code + + @property + def response_body(self) -> str: + """ + :return: The raw response body that was returned by the Worldline Acquiring platform. + """ + return self.__response_body + + @property + def type(self) -> Optional[str]: + """ + :return: The type received from the Worldline Acquiring platform if available. + """ + return self.__type + + @property + def title(self) -> Optional[str]: + """ + :return: The title received from the Worldline Acquiring platform if available. + """ + return self.__title + + @property + def status(self) -> Optional[int]: + """ + :return: The status received from the Worldline Acquiring platform if available. + """ + return self.__status + + @property + def detail(self) -> Optional[str]: + """ + :return: The detail received from the Worldline Acquiring platform if available. + """ + return self.__detail + + @property + def instance(self) -> Optional[str]: + """ + :return: The instance received from the Worldline Acquiring platform if available. + """ + return self.__instance + + def __str__(self): + string = super(ApiException, self).__str__() + if self.__status_code > 0: + string += "; status_code=" + str(self.__status_code) + if self.__response_body: + string += "; response_body='" + self.__response_body + "'" + return str(string) diff --git a/worldline/acquiring/sdk/v1/authorization_exception.py b/worldline/acquiring/sdk/v1/authorization_exception.py new file mode 100644 index 0000000..a86c28c --- /dev/null +++ b/worldline/acquiring/sdk/v1/authorization_exception.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_exception import ApiException + + +class AuthorizationException(ApiException): + """ + Represents an error response from the Worldline Acquiring platform when API authorization failed. + """ + + def __init__(self, status_code: int, response_body: str, type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str], + message: str = "The Worldline Acquiring platform returned an API authorization error response"): + super(AuthorizationException, self).__init__(status_code, response_body, type, title, status, detail, instance, message) diff --git a/worldline/acquiring/sdk/v1/domain/__init__.py b/worldline/acquiring/sdk/v1/domain/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/domain/address_verification_data.py b/worldline/acquiring/sdk/v1/domain/address_verification_data.py new file mode 100644 index 0000000..4f95f89 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/address_verification_data.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class AddressVerificationData(DataObject): + + __cardholder_address: Optional[str] = None + __cardholder_postal_code: Optional[str] = None + + @property + def cardholder_address(self) -> Optional[str]: + """ + | Cardholder street address + + Type: str + """ + return self.__cardholder_address + + @cardholder_address.setter + def cardholder_address(self, value: Optional[str]) -> None: + self.__cardholder_address = value + + @property + def cardholder_postal_code(self) -> Optional[str]: + """ + | Cardholder postal code, should be provided without spaces + + Type: str + """ + return self.__cardholder_postal_code + + @cardholder_postal_code.setter + def cardholder_postal_code(self, value: Optional[str]) -> None: + self.__cardholder_postal_code = value + + def to_dictionary(self) -> dict: + dictionary = super(AddressVerificationData, self).to_dictionary() + if self.cardholder_address is not None: + dictionary['cardholderAddress'] = self.cardholder_address + if self.cardholder_postal_code is not None: + dictionary['cardholderPostalCode'] = self.cardholder_postal_code + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'AddressVerificationData': + super(AddressVerificationData, self).from_dictionary(dictionary) + if 'cardholderAddress' in dictionary: + self.cardholder_address = dictionary['cardholderAddress'] + if 'cardholderPostalCode' in dictionary: + self.cardholder_postal_code = dictionary['cardholderPostalCode'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/amount_data.py b/worldline/acquiring/sdk/v1/domain/amount_data.py new file mode 100644 index 0000000..057d69e --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/amount_data.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class AmountData(DataObject): + + __amount: Optional[int] = None + __currency_code: Optional[str] = None + __number_of_decimals: Optional[int] = None + + @property + def amount(self) -> Optional[int]: + """ + | Amount of transaction formatted according to card scheme specifications. E.g. 100 for 1.00 EUR. Either this or amount must be present. + + Type: int + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[int]) -> None: + self.__amount = value + + @property + def currency_code(self) -> Optional[str]: + """ + | Alpha-numeric ISO 4217 currency code for transaction, e.g. EUR + + Type: str + """ + return self.__currency_code + + @currency_code.setter + def currency_code(self, value: Optional[str]) -> None: + self.__currency_code = value + + @property + def number_of_decimals(self) -> Optional[int]: + """ + | Number of decimals in the amount + + Type: int + """ + return self.__number_of_decimals + + @number_of_decimals.setter + def number_of_decimals(self, value: Optional[int]) -> None: + self.__number_of_decimals = value + + def to_dictionary(self) -> dict: + dictionary = super(AmountData, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount + if self.currency_code is not None: + dictionary['currencyCode'] = self.currency_code + if self.number_of_decimals is not None: + dictionary['numberOfDecimals'] = self.number_of_decimals + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'AmountData': + super(AmountData, self).from_dictionary(dictionary) + if 'amount' in dictionary: + self.amount = dictionary['amount'] + if 'currencyCode' in dictionary: + self.currency_code = dictionary['currencyCode'] + if 'numberOfDecimals' in dictionary: + self.number_of_decimals = dictionary['numberOfDecimals'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_account_verification_request.py b/worldline/acquiring/sdk/v1/domain/api_account_verification_request.py new file mode 100644 index 0000000..14fa857 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_account_verification_request.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .card_payment_data_for_verification import CardPaymentDataForVerification +from .merchant_data import MerchantData +from .payment_references import PaymentReferences + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiAccountVerificationRequest(DataObject): + + __card_payment_data: Optional[CardPaymentDataForVerification] = None + __merchant: Optional[MerchantData] = None + __operation_id: Optional[str] = None + __references: Optional[PaymentReferences] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForVerification]: + """ + | Card data + + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_verification.CardPaymentDataForVerification` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForVerification]) -> None: + self.__card_payment_data = value + + @property + def merchant(self) -> Optional[MerchantData]: + """ + | Merchant Data + + Type: :class:`worldline.acquiring.sdk.v1.domain.merchant_data.MerchantData` + """ + return self.__merchant + + @merchant.setter + def merchant(self, value: Optional[MerchantData]) -> None: + self.__merchant = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def references(self) -> Optional[PaymentReferences]: + """ + | Payment References + + Type: :class:`worldline.acquiring.sdk.v1.domain.payment_references.PaymentReferences` + """ + return self.__references + + @references.setter + def references(self, value: Optional[PaymentReferences]) -> None: + self.__references = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiAccountVerificationRequest, self).to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.merchant is not None: + dictionary['merchant'] = self.merchant.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiAccountVerificationRequest': + super(ApiAccountVerificationRequest, self).from_dictionary(dictionary) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForVerification() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'merchant' in dictionary: + if not isinstance(dictionary['merchant'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['merchant'])) + value = MerchantData() + self.merchant = value.from_dictionary(dictionary['merchant']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = PaymentReferences() + self.references = value.from_dictionary(dictionary['references']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_account_verification_response.py b/worldline/acquiring/sdk/v1/domain/api_account_verification_response.py new file mode 100644 index 0000000..5f88229 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_account_verification_response.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_references_for_responses import ApiReferencesForResponses +from .card_payment_data_for_response import CardPaymentDataForResponse + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiAccountVerificationResponse(DataObject): + + __authorization_code: Optional[str] = None + __card_payment_data: Optional[CardPaymentDataForResponse] = None + __operation_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + + @property + def authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__authorization_code + + @authorization_code.setter + def authorization_code(self, value: Optional[str]) -> None: + self.__authorization_code = value + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForResponse]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_response.CardPaymentDataForResponse` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForResponse]) -> None: + self.__card_payment_data = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiAccountVerificationResponse, self).to_dictionary() + if self.authorization_code is not None: + dictionary['authorizationCode'] = self.authorization_code + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiAccountVerificationResponse': + super(ApiAccountVerificationResponse, self).from_dictionary(dictionary) + if 'authorizationCode' in dictionary: + self.authorization_code = dictionary['authorizationCode'] + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForResponse() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_action_response.py b/worldline/acquiring/sdk/v1/domain/api_action_response.py new file mode 100644 index 0000000..dcaee59 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_action_response.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_payment_summary_for_response import ApiPaymentSummaryForResponse + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiActionResponse(DataObject): + + __operation_id: Optional[str] = None + __payment: Optional[ApiPaymentSummaryForResponse] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def payment(self) -> Optional[ApiPaymentSummaryForResponse]: + """ + | A summary of the payment used for responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_payment_summary_for_response.ApiPaymentSummaryForResponse` + """ + return self.__payment + + @payment.setter + def payment(self, value: Optional[ApiPaymentSummaryForResponse]) -> None: + self.__payment = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiActionResponse, self).to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.payment is not None: + dictionary['payment'] = self.payment.to_dictionary() + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiActionResponse': + super(ApiActionResponse, self).from_dictionary(dictionary) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'payment' in dictionary: + if not isinstance(dictionary['payment'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['payment'])) + value = ApiPaymentSummaryForResponse() + self.payment = value.from_dictionary(dictionary['payment']) + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.py b/worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.py new file mode 100644 index 0000000..b303f89 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_refund_summary_for_response import ApiRefundSummaryForResponse + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiActionResponseForRefund(DataObject): + + __operation_id: Optional[str] = None + __refund: Optional[ApiRefundSummaryForResponse] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def refund(self) -> Optional[ApiRefundSummaryForResponse]: + """ + | A summary of the refund used for responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_refund_summary_for_response.ApiRefundSummaryForResponse` + """ + return self.__refund + + @refund.setter + def refund(self, value: Optional[ApiRefundSummaryForResponse]) -> None: + self.__refund = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiActionResponseForRefund, self).to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.refund is not None: + dictionary['refund'] = self.refund.to_dictionary() + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiActionResponseForRefund': + super(ApiActionResponseForRefund, self).from_dictionary(dictionary) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'refund' in dictionary: + if not isinstance(dictionary['refund'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['refund'])) + value = ApiRefundSummaryForResponse() + self.refund = value.from_dictionary(dictionary['refund']) + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_capture_request.py b/worldline/acquiring/sdk/v1/domain/api_capture_request.py new file mode 100644 index 0000000..d406b9d --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_capture_request.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .dcc_data import DccData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiCaptureRequest(DataObject): + + __amount: Optional[AmountData] = None + __capture_sequence_number: Optional[int] = None + __dynamic_currency_conversion: Optional[DccData] = None + __is_final: Optional[bool] = None + __operation_id: Optional[str] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount to capture. If not provided, the full amount will be captured. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def capture_sequence_number(self) -> Optional[int]: + """ + | The index of the partial capture. Not needed for full capture. + + Type: int + """ + return self.__capture_sequence_number + + @capture_sequence_number.setter + def capture_sequence_number(self, value: Optional[int]) -> None: + self.__capture_sequence_number = value + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def is_final(self) -> Optional[bool]: + """ + | Indicates whether this partial capture is the final one. + | Not needed for full capture. + + Type: bool + """ + return self.__is_final + + @is_final.setter + def is_final(self, value: Optional[bool]) -> None: + self.__is_final = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiCaptureRequest, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.capture_sequence_number is not None: + dictionary['captureSequenceNumber'] = self.capture_sequence_number + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.is_final is not None: + dictionary['isFinal'] = self.is_final + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiCaptureRequest': + super(ApiCaptureRequest, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'captureSequenceNumber' in dictionary: + self.capture_sequence_number = dictionary['captureSequenceNumber'] + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'isFinal' in dictionary: + self.is_final = dictionary['isFinal'] + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.py b/worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.py new file mode 100644 index 0000000..608f1af --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiCaptureRequestForRefund(DataObject): + + __operation_id: Optional[str] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiCaptureRequestForRefund, self).to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiCaptureRequestForRefund': + super(ApiCaptureRequestForRefund, self).from_dictionary(dictionary) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_increment_request.py b/worldline/acquiring/sdk/v1/domain/api_increment_request.py new file mode 100644 index 0000000..3e906d4 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_increment_request.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .dcc_data import DccData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiIncrementRequest(DataObject): + + __dynamic_currency_conversion: Optional[DccData] = None + __increment_amount: Optional[AmountData] = None + __operation_id: Optional[str] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def increment_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__increment_amount + + @increment_amount.setter + def increment_amount(self, value: Optional[AmountData]) -> None: + self.__increment_amount = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiIncrementRequest, self).to_dictionary() + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.increment_amount is not None: + dictionary['incrementAmount'] = self.increment_amount.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiIncrementRequest': + super(ApiIncrementRequest, self).from_dictionary(dictionary) + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'incrementAmount' in dictionary: + if not isinstance(dictionary['incrementAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['incrementAmount'])) + value = AmountData() + self.increment_amount = value.from_dictionary(dictionary['incrementAmount']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_increment_response.py b/worldline/acquiring/sdk/v1/domain/api_increment_response.py new file mode 100644 index 0000000..1368659 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_increment_response.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .amount_data import AmountData +from .api_action_response import ApiActionResponse + + +class ApiIncrementResponse(ApiActionResponse): + + __authorization_code: Optional[str] = None + __total_authorized_amount: Optional[AmountData] = None + + @property + def authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__authorization_code + + @authorization_code.setter + def authorization_code(self, value: Optional[str]) -> None: + self.__authorization_code = value + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiIncrementResponse, self).to_dictionary() + if self.authorization_code is not None: + dictionary['authorizationCode'] = self.authorization_code + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiIncrementResponse': + super(ApiIncrementResponse, self).from_dictionary(dictionary) + if 'authorizationCode' in dictionary: + self.authorization_code = dictionary['authorizationCode'] + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_error_response.py b/worldline/acquiring/sdk/v1/domain/api_payment_error_response.py new file mode 100644 index 0000000..5e16e3e --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_error_response.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentErrorResponse(DataObject): + + __detail: Optional[str] = None + __instance: Optional[str] = None + __status: Optional[int] = None + __title: Optional[str] = None + __type: Optional[str] = None + + @property + def detail(self) -> Optional[str]: + """ + | Any relevant details about the error. + | May include suggestions for handling it. Can be an empty string if no extra details are available. + + Type: str + """ + return self.__detail + + @detail.setter + def detail(self, value: Optional[str]) -> None: + self.__detail = value + + @property + def instance(self) -> Optional[str]: + """ + | A URI reference that identifies the specific occurrence of the error. + | It may or may not yield further information if dereferenced. + + Type: str + """ + return self.__instance + + @instance.setter + def instance(self, value: Optional[str]) -> None: + self.__instance = value + + @property + def status(self) -> Optional[int]: + """ + | The HTTP status code of this error response. + | Included to aid those frameworks that have a hard time working with anything other than the body of an HTTP response. + + Type: int + """ + return self.__status + + @status.setter + def status(self, value: Optional[int]) -> None: + self.__status = value + + @property + def title(self) -> Optional[str]: + """ + | The human-readable version of the error. + + Type: str + """ + return self.__title + + @title.setter + def title(self, value: Optional[str]) -> None: + self.__title = value + + @property + def type(self) -> Optional[str]: + """ + | The type of the error. + | This is what you should match against when implementing error handling. + | It is in the form of a URL that identifies the error type. + + Type: str + """ + return self.__type + + @type.setter + def type(self, value: Optional[str]) -> None: + self.__type = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentErrorResponse, self).to_dictionary() + if self.detail is not None: + dictionary['detail'] = self.detail + if self.instance is not None: + dictionary['instance'] = self.instance + if self.status is not None: + dictionary['status'] = self.status + if self.title is not None: + dictionary['title'] = self.title + if self.type is not None: + dictionary['type'] = self.type + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentErrorResponse': + super(ApiPaymentErrorResponse, self).from_dictionary(dictionary) + if 'detail' in dictionary: + self.detail = dictionary['detail'] + if 'instance' in dictionary: + self.instance = dictionary['instance'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'title' in dictionary: + self.title = dictionary['title'] + if 'type' in dictionary: + self.type = dictionary['type'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_refund_request.py b/worldline/acquiring/sdk/v1/domain/api_payment_refund_request.py new file mode 100644 index 0000000..847e5d4 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_refund_request.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .dcc_data import DccData +from .payment_references import PaymentReferences + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentRefundRequest(DataObject): + + __amount: Optional[AmountData] = None + __capture_immediately: Optional[bool] = None + __dynamic_currency_conversion: Optional[DccData] = None + __operation_id: Optional[str] = None + __references: Optional[PaymentReferences] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount to refund. If not provided, the full amount will be refunded. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def capture_immediately(self) -> Optional[bool]: + """ + | If true the transaction will be authorized and captured immediately + + Type: bool + """ + return self.__capture_immediately + + @capture_immediately.setter + def capture_immediately(self, value: Optional[bool]) -> None: + self.__capture_immediately = value + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def references(self) -> Optional[PaymentReferences]: + """ + | Payment References + + Type: :class:`worldline.acquiring.sdk.v1.domain.payment_references.PaymentReferences` + """ + return self.__references + + @references.setter + def references(self, value: Optional[PaymentReferences]) -> None: + self.__references = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentRefundRequest, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.capture_immediately is not None: + dictionary['captureImmediately'] = self.capture_immediately + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentRefundRequest': + super(ApiPaymentRefundRequest, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'captureImmediately' in dictionary: + self.capture_immediately = dictionary['captureImmediately'] + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = PaymentReferences() + self.references = value.from_dictionary(dictionary['references']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_request.py b/worldline/acquiring/sdk/v1/domain/api_payment_request.py new file mode 100644 index 0000000..b4be933 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_request.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .card_payment_data import CardPaymentData +from .dcc_data import DccData +from .merchant_data import MerchantData +from .payment_references import PaymentReferences + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentRequest(DataObject): + + __amount: Optional[AmountData] = None + __authorization_type: Optional[str] = None + __card_payment_data: Optional[CardPaymentData] = None + __dynamic_currency_conversion: Optional[DccData] = None + __merchant: Optional[MerchantData] = None + __operation_id: Optional[str] = None + __references: Optional[PaymentReferences] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def authorization_type(self) -> Optional[str]: + """ + | The type of authorization + + Type: str + """ + return self.__authorization_type + + @authorization_type.setter + def authorization_type(self, value: Optional[str]) -> None: + self.__authorization_type = value + + @property + def card_payment_data(self) -> Optional[CardPaymentData]: + """ + | Card data + + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data.CardPaymentData` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentData]) -> None: + self.__card_payment_data = value + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def merchant(self) -> Optional[MerchantData]: + """ + | Merchant Data + + Type: :class:`worldline.acquiring.sdk.v1.domain.merchant_data.MerchantData` + """ + return self.__merchant + + @merchant.setter + def merchant(self, value: Optional[MerchantData]) -> None: + self.__merchant = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def references(self) -> Optional[PaymentReferences]: + """ + | Payment References + + Type: :class:`worldline.acquiring.sdk.v1.domain.payment_references.PaymentReferences` + """ + return self.__references + + @references.setter + def references(self, value: Optional[PaymentReferences]) -> None: + self.__references = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentRequest, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.authorization_type is not None: + dictionary['authorizationType'] = self.authorization_type + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.merchant is not None: + dictionary['merchant'] = self.merchant.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentRequest': + super(ApiPaymentRequest, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'authorizationType' in dictionary: + self.authorization_type = dictionary['authorizationType'] + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentData() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'merchant' in dictionary: + if not isinstance(dictionary['merchant'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['merchant'])) + value = MerchantData() + self.merchant = value.from_dictionary(dictionary['merchant']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = PaymentReferences() + self.references = value.from_dictionary(dictionary['references']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_resource.py b/worldline/acquiring/sdk/v1/domain/api_payment_resource.py new file mode 100644 index 0000000..dc22131 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_resource.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import List, Optional + +from .amount_data import AmountData +from .api_references_for_responses import ApiReferencesForResponses +from .card_payment_data_for_resource import CardPaymentDataForResource +from .sub_operation import SubOperation + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentResource(DataObject): + + __card_payment_data: Optional[CardPaymentDataForResource] = None + __initial_authorization_code: Optional[str] = None + __operations: Optional[List[SubOperation]] = None + __payment_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + __total_authorized_amount: Optional[AmountData] = None + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForResource]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_resource.CardPaymentDataForResource` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForResource]) -> None: + self.__card_payment_data = value + + @property + def initial_authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__initial_authorization_code + + @initial_authorization_code.setter + def initial_authorization_code(self, value: Optional[str]) -> None: + self.__initial_authorization_code = value + + @property + def operations(self) -> Optional[List[SubOperation]]: + """ + Type: list[:class:`worldline.acquiring.sdk.v1.domain.sub_operation.SubOperation`] + """ + return self.__operations + + @operations.setter + def operations(self, value: Optional[List[SubOperation]]) -> None: + self.__operations = value + + @property + def payment_id(self) -> Optional[str]: + """ + | the ID of the payment + + Type: str + """ + return self.__payment_id + + @payment_id.setter + def payment_id(self, value: Optional[str]) -> None: + self.__payment_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentResource, self).to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.initial_authorization_code is not None: + dictionary['initialAuthorizationCode'] = self.initial_authorization_code + if self.operations is not None: + dictionary['operations'] = [] + for element in self.operations: + if element is not None: + dictionary['operations'].append(element.to_dictionary()) + if self.payment_id is not None: + dictionary['paymentId'] = self.payment_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentResource': + super(ApiPaymentResource, self).from_dictionary(dictionary) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForResource() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'initialAuthorizationCode' in dictionary: + self.initial_authorization_code = dictionary['initialAuthorizationCode'] + if 'operations' in dictionary: + if not isinstance(dictionary['operations'], list): + raise TypeError('value \'{}\' is not a list'.format(dictionary['operations'])) + self.operations = [] + for element in dictionary['operations']: + value = SubOperation() + self.operations.append(value.from_dictionary(element)) + if 'paymentId' in dictionary: + self.payment_id = dictionary['paymentId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_response.py b/worldline/acquiring/sdk/v1/domain/api_payment_response.py new file mode 100644 index 0000000..f776e7d --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_response.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .api_references_for_responses import ApiReferencesForResponses +from .card_payment_data_for_response import CardPaymentDataForResponse + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentResponse(DataObject): + + __card_payment_data: Optional[CardPaymentDataForResponse] = None + __initial_authorization_code: Optional[str] = None + __operation_id: Optional[str] = None + __payment_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + __total_authorized_amount: Optional[AmountData] = None + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForResponse]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_response.CardPaymentDataForResponse` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForResponse]) -> None: + self.__card_payment_data = value + + @property + def initial_authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__initial_authorization_code + + @initial_authorization_code.setter + def initial_authorization_code(self, value: Optional[str]) -> None: + self.__initial_authorization_code = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def payment_id(self) -> Optional[str]: + """ + | the ID of the payment + + Type: str + """ + return self.__payment_id + + @payment_id.setter + def payment_id(self, value: Optional[str]) -> None: + self.__payment_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentResponse, self).to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.initial_authorization_code is not None: + dictionary['initialAuthorizationCode'] = self.initial_authorization_code + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.payment_id is not None: + dictionary['paymentId'] = self.payment_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentResponse': + super(ApiPaymentResponse, self).from_dictionary(dictionary) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForResponse() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'initialAuthorizationCode' in dictionary: + self.initial_authorization_code = dictionary['initialAuthorizationCode'] + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'paymentId' in dictionary: + self.payment_id = dictionary['paymentId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.py b/worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.py new file mode 100644 index 0000000..b3b20b0 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .dcc_data import DccData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentReversalRequest(DataObject): + + __dynamic_currency_conversion: Optional[DccData] = None + __operation_id: Optional[str] = None + __reversal_amount: Optional[AmountData] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def reversal_amount(self) -> Optional[AmountData]: + """ + | Amount to reverse. If not provided, the full amount will be reversed. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__reversal_amount + + @reversal_amount.setter + def reversal_amount(self, value: Optional[AmountData]) -> None: + self.__reversal_amount = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentReversalRequest, self).to_dictionary() + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.reversal_amount is not None: + dictionary['reversalAmount'] = self.reversal_amount.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentReversalRequest': + super(ApiPaymentReversalRequest, self).from_dictionary(dictionary) + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'reversalAmount' in dictionary: + if not isinstance(dictionary['reversalAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['reversalAmount'])) + value = AmountData() + self.reversal_amount = value.from_dictionary(dictionary['reversalAmount']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.py b/worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.py new file mode 100644 index 0000000..c3c9967 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .api_references_for_responses import ApiReferencesForResponses + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiPaymentSummaryForResponse(DataObject): + + __payment_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + + @property + def payment_id(self) -> Optional[str]: + """ + | the ID of the payment + + Type: str + """ + return self.__payment_id + + @payment_id.setter + def payment_id(self, value: Optional[str]) -> None: + self.__payment_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiPaymentSummaryForResponse, self).to_dictionary() + if self.payment_id is not None: + dictionary['paymentId'] = self.payment_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiPaymentSummaryForResponse': + super(ApiPaymentSummaryForResponse, self).from_dictionary(dictionary) + if 'paymentId' in dictionary: + self.payment_id = dictionary['paymentId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_references_for_responses.py b/worldline/acquiring/sdk/v1/domain/api_references_for_responses.py new file mode 100644 index 0000000..25c075f --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_references_for_responses.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiReferencesForResponses(DataObject): + + __payment_account_reference: Optional[str] = None + __retrieval_reference_number: Optional[str] = None + __scheme_transaction_id: Optional[str] = None + + @property + def payment_account_reference(self) -> Optional[str]: + """ + | (PAR) Unique identifier associated with a specific cardholder PAN + + Type: str + """ + return self.__payment_account_reference + + @payment_account_reference.setter + def payment_account_reference(self, value: Optional[str]) -> None: + self.__payment_account_reference = value + + @property + def retrieval_reference_number(self) -> Optional[str]: + """ + | Retrieval reference number for transaction, must be AN(12) if provided + + Type: str + """ + return self.__retrieval_reference_number + + @retrieval_reference_number.setter + def retrieval_reference_number(self, value: Optional[str]) -> None: + self.__retrieval_reference_number = value + + @property + def scheme_transaction_id(self) -> Optional[str]: + """ + | ID assigned by the scheme to identify a transaction through its whole lifecycle. + + Type: str + """ + return self.__scheme_transaction_id + + @scheme_transaction_id.setter + def scheme_transaction_id(self, value: Optional[str]) -> None: + self.__scheme_transaction_id = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiReferencesForResponses, self).to_dictionary() + if self.payment_account_reference is not None: + dictionary['paymentAccountReference'] = self.payment_account_reference + if self.retrieval_reference_number is not None: + dictionary['retrievalReferenceNumber'] = self.retrieval_reference_number + if self.scheme_transaction_id is not None: + dictionary['schemeTransactionId'] = self.scheme_transaction_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiReferencesForResponses': + super(ApiReferencesForResponses, self).from_dictionary(dictionary) + if 'paymentAccountReference' in dictionary: + self.payment_account_reference = dictionary['paymentAccountReference'] + if 'retrievalReferenceNumber' in dictionary: + self.retrieval_reference_number = dictionary['retrievalReferenceNumber'] + if 'schemeTransactionId' in dictionary: + self.scheme_transaction_id = dictionary['schemeTransactionId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_refund_request.py b/worldline/acquiring/sdk/v1/domain/api_refund_request.py new file mode 100644 index 0000000..2d78b24 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_refund_request.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .card_payment_data_for_refund import CardPaymentDataForRefund +from .dcc_data import DccData +from .merchant_data import MerchantData +from .payment_references import PaymentReferences + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiRefundRequest(DataObject): + + __amount: Optional[AmountData] = None + __card_payment_data: Optional[CardPaymentDataForRefund] = None + __dynamic_currency_conversion: Optional[DccData] = None + __merchant: Optional[MerchantData] = None + __operation_id: Optional[str] = None + __references: Optional[PaymentReferences] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForRefund]: + """ + | Card data for refund + + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_refund.CardPaymentDataForRefund` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForRefund]) -> None: + self.__card_payment_data = value + + @property + def dynamic_currency_conversion(self) -> Optional[DccData]: + """ + | Dynamic Currency Conversion (DCC) rate data from DCC lookup response. + | Mandatory for DCC transactions. + + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_data.DccData` + """ + return self.__dynamic_currency_conversion + + @dynamic_currency_conversion.setter + def dynamic_currency_conversion(self, value: Optional[DccData]) -> None: + self.__dynamic_currency_conversion = value + + @property + def merchant(self) -> Optional[MerchantData]: + """ + | Merchant Data + + Type: :class:`worldline.acquiring.sdk.v1.domain.merchant_data.MerchantData` + """ + return self.__merchant + + @merchant.setter + def merchant(self, value: Optional[MerchantData]) -> None: + self.__merchant = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def references(self) -> Optional[PaymentReferences]: + """ + | Payment References + + Type: :class:`worldline.acquiring.sdk.v1.domain.payment_references.PaymentReferences` + """ + return self.__references + + @references.setter + def references(self, value: Optional[PaymentReferences]) -> None: + self.__references = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiRefundRequest, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.dynamic_currency_conversion is not None: + dictionary['dynamicCurrencyConversion'] = self.dynamic_currency_conversion.to_dictionary() + if self.merchant is not None: + dictionary['merchant'] = self.merchant.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiRefundRequest': + super(ApiRefundRequest, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForRefund() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'dynamicCurrencyConversion' in dictionary: + if not isinstance(dictionary['dynamicCurrencyConversion'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['dynamicCurrencyConversion'])) + value = DccData() + self.dynamic_currency_conversion = value.from_dictionary(dictionary['dynamicCurrencyConversion']) + if 'merchant' in dictionary: + if not isinstance(dictionary['merchant'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['merchant'])) + value = MerchantData() + self.merchant = value.from_dictionary(dictionary['merchant']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = PaymentReferences() + self.references = value.from_dictionary(dictionary['references']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_refund_resource.py b/worldline/acquiring/sdk/v1/domain/api_refund_resource.py new file mode 100644 index 0000000..755cf26 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_refund_resource.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import List, Optional + +from .amount_data import AmountData +from .api_references_for_responses import ApiReferencesForResponses +from .card_payment_data_for_resource import CardPaymentDataForResource +from .sub_operation_for_refund import SubOperationForRefund + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiRefundResource(DataObject): + + __card_payment_data: Optional[CardPaymentDataForResource] = None + __initial_authorization_code: Optional[str] = None + __operations: Optional[List[SubOperationForRefund]] = None + __referenced_payment_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __refund_id: Optional[str] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + __total_authorized_amount: Optional[AmountData] = None + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForResource]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_resource.CardPaymentDataForResource` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForResource]) -> None: + self.__card_payment_data = value + + @property + def initial_authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__initial_authorization_code + + @initial_authorization_code.setter + def initial_authorization_code(self, value: Optional[str]) -> None: + self.__initial_authorization_code = value + + @property + def operations(self) -> Optional[List[SubOperationForRefund]]: + """ + Type: list[:class:`worldline.acquiring.sdk.v1.domain.sub_operation_for_refund.SubOperationForRefund`] + """ + return self.__operations + + @operations.setter + def operations(self, value: Optional[List[SubOperationForRefund]]) -> None: + self.__operations = value + + @property + def referenced_payment_id(self) -> Optional[str]: + """ + | The identifier of the payment referenced by this refund. + + Type: str + """ + return self.__referenced_payment_id + + @referenced_payment_id.setter + def referenced_payment_id(self, value: Optional[str]) -> None: + self.__referenced_payment_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def refund_id(self) -> Optional[str]: + """ + | the ID of the refund + + Type: str + """ + return self.__refund_id + + @refund_id.setter + def refund_id(self, value: Optional[str]) -> None: + self.__refund_id = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiRefundResource, self).to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.initial_authorization_code is not None: + dictionary['initialAuthorizationCode'] = self.initial_authorization_code + if self.operations is not None: + dictionary['operations'] = [] + for element in self.operations: + if element is not None: + dictionary['operations'].append(element.to_dictionary()) + if self.referenced_payment_id is not None: + dictionary['referencedPaymentId'] = self.referenced_payment_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.refund_id is not None: + dictionary['refundId'] = self.refund_id + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiRefundResource': + super(ApiRefundResource, self).from_dictionary(dictionary) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForResource() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'initialAuthorizationCode' in dictionary: + self.initial_authorization_code = dictionary['initialAuthorizationCode'] + if 'operations' in dictionary: + if not isinstance(dictionary['operations'], list): + raise TypeError('value \'{}\' is not a list'.format(dictionary['operations'])) + self.operations = [] + for element in dictionary['operations']: + value = SubOperationForRefund() + self.operations.append(value.from_dictionary(element)) + if 'referencedPaymentId' in dictionary: + self.referenced_payment_id = dictionary['referencedPaymentId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'refundId' in dictionary: + self.refund_id = dictionary['refundId'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_refund_response.py b/worldline/acquiring/sdk/v1/domain/api_refund_response.py new file mode 100644 index 0000000..a8ec8f0 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_refund_response.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData +from .api_references_for_responses import ApiReferencesForResponses +from .card_payment_data_for_resource import CardPaymentDataForResource + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiRefundResponse(DataObject): + + __authorization_code: Optional[str] = None + __card_payment_data: Optional[CardPaymentDataForResource] = None + __operation_id: Optional[str] = None + __referenced_payment_id: Optional[str] = None + __references: Optional[ApiReferencesForResponses] = None + __refund_id: Optional[str] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + __total_authorized_amount: Optional[AmountData] = None + + @property + def authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__authorization_code + + @authorization_code.setter + def authorization_code(self, value: Optional[str]) -> None: + self.__authorization_code = value + + @property + def card_payment_data(self) -> Optional[CardPaymentDataForResource]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_payment_data_for_resource.CardPaymentDataForResource` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardPaymentDataForResource]) -> None: + self.__card_payment_data = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def referenced_payment_id(self) -> Optional[str]: + """ + | The identifier of the payment referenced by this refund. + + Type: str + """ + return self.__referenced_payment_id + + @referenced_payment_id.setter + def referenced_payment_id(self, value: Optional[str]) -> None: + self.__referenced_payment_id = value + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def refund_id(self) -> Optional[str]: + """ + | the ID of the refund + + Type: str + """ + return self.__refund_id + + @refund_id.setter + def refund_id(self, value: Optional[str]) -> None: + self.__refund_id = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiRefundResponse, self).to_dictionary() + if self.authorization_code is not None: + dictionary['authorizationCode'] = self.authorization_code + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.referenced_payment_id is not None: + dictionary['referencedPaymentId'] = self.referenced_payment_id + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.refund_id is not None: + dictionary['refundId'] = self.refund_id + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiRefundResponse': + super(ApiRefundResponse, self).from_dictionary(dictionary) + if 'authorizationCode' in dictionary: + self.authorization_code = dictionary['authorizationCode'] + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardPaymentDataForResource() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'referencedPaymentId' in dictionary: + self.referenced_payment_id = dictionary['referencedPaymentId'] + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'refundId' in dictionary: + self.refund_id = dictionary['refundId'] + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.py b/worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.py new file mode 100644 index 0000000..c5d61ff --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .api_references_for_responses import ApiReferencesForResponses + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiRefundSummaryForResponse(DataObject): + + __references: Optional[ApiReferencesForResponses] = None + __refund_id: Optional[str] = None + __retry_after: Optional[str] = None + __status: Optional[str] = None + __status_timestamp: Optional[datetime] = None + + @property + def references(self) -> Optional[ApiReferencesForResponses]: + """ + | A set of references returned in responses + + Type: :class:`worldline.acquiring.sdk.v1.domain.api_references_for_responses.ApiReferencesForResponses` + """ + return self.__references + + @references.setter + def references(self, value: Optional[ApiReferencesForResponses]) -> None: + self.__references = value + + @property + def refund_id(self) -> Optional[str]: + """ + | the ID of the refund + + Type: str + """ + return self.__refund_id + + @refund_id.setter + def refund_id(self, value: Optional[str]) -> None: + self.__refund_id = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the payment. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the payment can be retried later. + | PT0 means that the payment can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + @property + def status(self) -> Optional[str]: + """ + | The status of the payment, refund or credit transfer + + Type: str + """ + return self.__status + + @status.setter + def status(self, value: Optional[str]) -> None: + self.__status = value + + @property + def status_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the status in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__status_timestamp + + @status_timestamp.setter + def status_timestamp(self, value: Optional[datetime]) -> None: + self.__status_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiRefundSummaryForResponse, self).to_dictionary() + if self.references is not None: + dictionary['references'] = self.references.to_dictionary() + if self.refund_id is not None: + dictionary['refundId'] = self.refund_id + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + if self.status is not None: + dictionary['status'] = self.status + if self.status_timestamp is not None: + dictionary['statusTimestamp'] = DataObject.format_datetime(self.status_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiRefundSummaryForResponse': + super(ApiRefundSummaryForResponse, self).from_dictionary(dictionary) + if 'references' in dictionary: + if not isinstance(dictionary['references'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['references'])) + value = ApiReferencesForResponses() + self.references = value.from_dictionary(dictionary['references']) + if 'refundId' in dictionary: + self.refund_id = dictionary['refundId'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + if 'status' in dictionary: + self.status = dictionary['status'] + if 'statusTimestamp' in dictionary: + self.status_timestamp = DataObject.parse_datetime(dictionary['statusTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_reversal_response.py b/worldline/acquiring/sdk/v1/domain/api_reversal_response.py new file mode 100644 index 0000000..a78ce4a --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_reversal_response.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .amount_data import AmountData +from .api_action_response import ApiActionResponse + + +class ApiReversalResponse(ApiActionResponse): + + __total_authorized_amount: Optional[AmountData] = None + + @property + def total_authorized_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__total_authorized_amount + + @total_authorized_amount.setter + def total_authorized_amount(self, value: Optional[AmountData]) -> None: + self.__total_authorized_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiReversalResponse, self).to_dictionary() + if self.total_authorized_amount is not None: + dictionary['totalAuthorizedAmount'] = self.total_authorized_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiReversalResponse': + super(ApiReversalResponse, self).from_dictionary(dictionary) + if 'totalAuthorizedAmount' in dictionary: + if not isinstance(dictionary['totalAuthorizedAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['totalAuthorizedAmount'])) + value = AmountData() + self.total_authorized_amount = value.from_dictionary(dictionary['totalAuthorizedAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.py b/worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.py new file mode 100644 index 0000000..53ee090 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiTechnicalReversalRequest(DataObject): + + __operation_id: Optional[str] = None + __reason: Optional[str] = None + __transaction_timestamp: Optional[datetime] = None + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def reason(self) -> Optional[str]: + """ + | Reason for reversal + + Type: str + """ + return self.__reason + + @reason.setter + def reason(self, value: Optional[str]) -> None: + self.__reason = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of transaction in ISO 8601 format (YYYY-MM-DDThh:mm:ss+TZD) + | It can be expressed in merchant time zone (ex: 2023-10-10T08:00+02:00) or in UTC (ex: 2023-10-10T08:00Z) + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiTechnicalReversalRequest, self).to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.reason is not None: + dictionary['reason'] = self.reason + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiTechnicalReversalRequest': + super(ApiTechnicalReversalRequest, self).from_dictionary(dictionary) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'reason' in dictionary: + self.reason = dictionary['reason'] + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.py b/worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.py new file mode 100644 index 0000000..639227b --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ApiTechnicalReversalResponse(DataObject): + + __operation_id: Optional[str] = None + __responder: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def responder(self) -> Optional[str]: + """ + | The party that originated the response + + Type: str + """ + return self.__responder + + @responder.setter + def responder(self, value: Optional[str]) -> None: + self.__responder = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + def to_dictionary(self) -> dict: + dictionary = super(ApiTechnicalReversalResponse, self).to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.responder is not None: + dictionary['responder'] = self.responder + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ApiTechnicalReversalResponse': + super(ApiTechnicalReversalResponse, self).from_dictionary(dictionary) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'responder' in dictionary: + self.responder = dictionary['responder'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_data_for_dcc.py b/worldline/acquiring/sdk/v1/domain/card_data_for_dcc.py new file mode 100644 index 0000000..e6c5481 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_data_for_dcc.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardDataForDcc(DataObject): + + __bin: Optional[str] = None + __brand: Optional[str] = None + __card_country_code: Optional[str] = None + + @property + def bin(self) -> Optional[str]: + """ + | Used to determine the currency of the card. The first 12 digits of the card number. The BIN number is on the first 6 or 8 digits. Some issuers are using subranges for different countries on digits 9-12. + + Type: str + """ + return self.__bin + + @bin.setter + def bin(self, value: Optional[str]) -> None: + self.__bin = value + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def card_country_code(self) -> Optional[str]: + """ + | The country code of the card + + Type: str + """ + return self.__card_country_code + + @card_country_code.setter + def card_country_code(self, value: Optional[str]) -> None: + self.__card_country_code = value + + def to_dictionary(self) -> dict: + dictionary = super(CardDataForDcc, self).to_dictionary() + if self.bin is not None: + dictionary['bin'] = self.bin + if self.brand is not None: + dictionary['brand'] = self.brand + if self.card_country_code is not None: + dictionary['cardCountryCode'] = self.card_country_code + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardDataForDcc': + super(CardDataForDcc, self).from_dictionary(dictionary) + if 'bin' in dictionary: + self.bin = dictionary['bin'] + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'cardCountryCode' in dictionary: + self.card_country_code = dictionary['cardCountryCode'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_on_file_data.py b/worldline/acquiring/sdk/v1/domain/card_on_file_data.py new file mode 100644 index 0000000..436e698 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_on_file_data.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .initial_card_on_file_data import InitialCardOnFileData +from .subsequent_card_on_file_data import SubsequentCardOnFileData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardOnFileData(DataObject): + + __initial_card_on_file_data: Optional[InitialCardOnFileData] = None + __is_initial_transaction: Optional[bool] = None + __subsequent_card_on_file_data: Optional[SubsequentCardOnFileData] = None + + @property + def initial_card_on_file_data(self) -> Optional[InitialCardOnFileData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.initial_card_on_file_data.InitialCardOnFileData` + """ + return self.__initial_card_on_file_data + + @initial_card_on_file_data.setter + def initial_card_on_file_data(self, value: Optional[InitialCardOnFileData]) -> None: + self.__initial_card_on_file_data = value + + @property + def is_initial_transaction(self) -> Optional[bool]: + """ + | Indicate wether this is the initial Card on File transaction or not + + Type: bool + """ + return self.__is_initial_transaction + + @is_initial_transaction.setter + def is_initial_transaction(self, value: Optional[bool]) -> None: + self.__is_initial_transaction = value + + @property + def subsequent_card_on_file_data(self) -> Optional[SubsequentCardOnFileData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.subsequent_card_on_file_data.SubsequentCardOnFileData` + """ + return self.__subsequent_card_on_file_data + + @subsequent_card_on_file_data.setter + def subsequent_card_on_file_data(self, value: Optional[SubsequentCardOnFileData]) -> None: + self.__subsequent_card_on_file_data = value + + def to_dictionary(self) -> dict: + dictionary = super(CardOnFileData, self).to_dictionary() + if self.initial_card_on_file_data is not None: + dictionary['initialCardOnFileData'] = self.initial_card_on_file_data.to_dictionary() + if self.is_initial_transaction is not None: + dictionary['isInitialTransaction'] = self.is_initial_transaction + if self.subsequent_card_on_file_data is not None: + dictionary['subsequentCardOnFileData'] = self.subsequent_card_on_file_data.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardOnFileData': + super(CardOnFileData, self).from_dictionary(dictionary) + if 'initialCardOnFileData' in dictionary: + if not isinstance(dictionary['initialCardOnFileData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['initialCardOnFileData'])) + value = InitialCardOnFileData() + self.initial_card_on_file_data = value.from_dictionary(dictionary['initialCardOnFileData']) + if 'isInitialTransaction' in dictionary: + self.is_initial_transaction = dictionary['isInitialTransaction'] + if 'subsequentCardOnFileData' in dictionary: + if not isinstance(dictionary['subsequentCardOnFileData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['subsequentCardOnFileData'])) + value = SubsequentCardOnFileData() + self.subsequent_card_on_file_data = value.from_dictionary(dictionary['subsequentCardOnFileData']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_payment_data.py b/worldline/acquiring/sdk/v1/domain/card_payment_data.py new file mode 100644 index 0000000..d32ae78 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_payment_data.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .card_on_file_data import CardOnFileData +from .e_commerce_data import ECommerceData +from .network_token_data import NetworkTokenData +from .plain_card_data import PlainCardData +from .point_of_sale_data import PointOfSaleData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardPaymentData(DataObject): + + __allow_partial_approval: Optional[bool] = None + __brand: Optional[str] = None + __capture_immediately: Optional[bool] = None + __card_data: Optional[PlainCardData] = None + __card_entry_mode: Optional[str] = None + __card_on_file_data: Optional[CardOnFileData] = None + __cardholder_verification_method: Optional[str] = None + __ecommerce_data: Optional[ECommerceData] = None + __network_token_data: Optional[NetworkTokenData] = None + __point_of_sale_data: Optional[PointOfSaleData] = None + __wallet_id: Optional[str] = None + + @property + def allow_partial_approval(self) -> Optional[bool]: + """ + | Indicate wether you allow partial approval or not + + Type: bool + """ + return self.__allow_partial_approval + + @allow_partial_approval.setter + def allow_partial_approval(self, value: Optional[bool]) -> None: + self.__allow_partial_approval = value + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def capture_immediately(self) -> Optional[bool]: + """ + | If true the transaction will be authorized and captured immediately + + Type: bool + """ + return self.__capture_immediately + + @capture_immediately.setter + def capture_immediately(self, value: Optional[bool]) -> None: + self.__capture_immediately = value + + @property + def card_data(self) -> Optional[PlainCardData]: + """ + | Card data in plain text + + Type: :class:`worldline.acquiring.sdk.v1.domain.plain_card_data.PlainCardData` + """ + return self.__card_data + + @card_data.setter + def card_data(self, value: Optional[PlainCardData]) -> None: + self.__card_data = value + + @property + def card_entry_mode(self) -> Optional[str]: + """ + | Card entry mode used in the transaction, defaults to ECOMMERCE + + Type: str + """ + return self.__card_entry_mode + + @card_entry_mode.setter + def card_entry_mode(self, value: Optional[str]) -> None: + self.__card_entry_mode = value + + @property + def card_on_file_data(self) -> Optional[CardOnFileData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_on_file_data.CardOnFileData` + """ + return self.__card_on_file_data + + @card_on_file_data.setter + def card_on_file_data(self, value: Optional[CardOnFileData]) -> None: + self.__card_on_file_data = value + + @property + def cardholder_verification_method(self) -> Optional[str]: + """ + | Cardholder verification method used in the transaction + + Type: str + """ + return self.__cardholder_verification_method + + @cardholder_verification_method.setter + def cardholder_verification_method(self, value: Optional[str]) -> None: + self.__cardholder_verification_method = value + + @property + def ecommerce_data(self) -> Optional[ECommerceData]: + """ + | Request data for eCommerce and MOTO transactions + + Type: :class:`worldline.acquiring.sdk.v1.domain.e_commerce_data.ECommerceData` + """ + return self.__ecommerce_data + + @ecommerce_data.setter + def ecommerce_data(self, value: Optional[ECommerceData]) -> None: + self.__ecommerce_data = value + + @property + def network_token_data(self) -> Optional[NetworkTokenData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.network_token_data.NetworkTokenData` + """ + return self.__network_token_data + + @network_token_data.setter + def network_token_data(self, value: Optional[NetworkTokenData]) -> None: + self.__network_token_data = value + + @property + def point_of_sale_data(self) -> Optional[PointOfSaleData]: + """ + | Payment terminal request data + + Type: :class:`worldline.acquiring.sdk.v1.domain.point_of_sale_data.PointOfSaleData` + """ + return self.__point_of_sale_data + + @point_of_sale_data.setter + def point_of_sale_data(self, value: Optional[PointOfSaleData]) -> None: + self.__point_of_sale_data = value + + @property + def wallet_id(self) -> Optional[str]: + """ + | Type of wallet, values are assigned by card schemes, e.g. 101 for MasterPass in eCommerce, 102 for MasterPass NFC, 103 for Apple Pay, 216 for Google Pay and 217 for Samsung Pay + + Type: str + """ + return self.__wallet_id + + @wallet_id.setter + def wallet_id(self, value: Optional[str]) -> None: + self.__wallet_id = value + + def to_dictionary(self) -> dict: + dictionary = super(CardPaymentData, self).to_dictionary() + if self.allow_partial_approval is not None: + dictionary['allowPartialApproval'] = self.allow_partial_approval + if self.brand is not None: + dictionary['brand'] = self.brand + if self.capture_immediately is not None: + dictionary['captureImmediately'] = self.capture_immediately + if self.card_data is not None: + dictionary['cardData'] = self.card_data.to_dictionary() + if self.card_entry_mode is not None: + dictionary['cardEntryMode'] = self.card_entry_mode + if self.card_on_file_data is not None: + dictionary['cardOnFileData'] = self.card_on_file_data.to_dictionary() + if self.cardholder_verification_method is not None: + dictionary['cardholderVerificationMethod'] = self.cardholder_verification_method + if self.ecommerce_data is not None: + dictionary['ecommerceData'] = self.ecommerce_data.to_dictionary() + if self.network_token_data is not None: + dictionary['networkTokenData'] = self.network_token_data.to_dictionary() + if self.point_of_sale_data is not None: + dictionary['pointOfSaleData'] = self.point_of_sale_data.to_dictionary() + if self.wallet_id is not None: + dictionary['walletId'] = self.wallet_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardPaymentData': + super(CardPaymentData, self).from_dictionary(dictionary) + if 'allowPartialApproval' in dictionary: + self.allow_partial_approval = dictionary['allowPartialApproval'] + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'captureImmediately' in dictionary: + self.capture_immediately = dictionary['captureImmediately'] + if 'cardData' in dictionary: + if not isinstance(dictionary['cardData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardData'])) + value = PlainCardData() + self.card_data = value.from_dictionary(dictionary['cardData']) + if 'cardEntryMode' in dictionary: + self.card_entry_mode = dictionary['cardEntryMode'] + if 'cardOnFileData' in dictionary: + if not isinstance(dictionary['cardOnFileData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardOnFileData'])) + value = CardOnFileData() + self.card_on_file_data = value.from_dictionary(dictionary['cardOnFileData']) + if 'cardholderVerificationMethod' in dictionary: + self.cardholder_verification_method = dictionary['cardholderVerificationMethod'] + if 'ecommerceData' in dictionary: + if not isinstance(dictionary['ecommerceData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['ecommerceData'])) + value = ECommerceData() + self.ecommerce_data = value.from_dictionary(dictionary['ecommerceData']) + if 'networkTokenData' in dictionary: + if not isinstance(dictionary['networkTokenData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['networkTokenData'])) + value = NetworkTokenData() + self.network_token_data = value.from_dictionary(dictionary['networkTokenData']) + if 'pointOfSaleData' in dictionary: + if not isinstance(dictionary['pointOfSaleData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['pointOfSaleData'])) + value = PointOfSaleData() + self.point_of_sale_data = value.from_dictionary(dictionary['pointOfSaleData']) + if 'walletId' in dictionary: + self.wallet_id = dictionary['walletId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.py b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.py new file mode 100644 index 0000000..3d9d51a --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .network_token_data import NetworkTokenData +from .plain_card_data import PlainCardData +from .point_of_sale_data import PointOfSaleData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardPaymentDataForRefund(DataObject): + + __brand: Optional[str] = None + __capture_immediately: Optional[bool] = None + __card_data: Optional[PlainCardData] = None + __card_entry_mode: Optional[str] = None + __network_token_data: Optional[NetworkTokenData] = None + __point_of_sale_data: Optional[PointOfSaleData] = None + __wallet_id: Optional[str] = None + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def capture_immediately(self) -> Optional[bool]: + """ + | If true the transaction will be authorized and captured immediately + + Type: bool + """ + return self.__capture_immediately + + @capture_immediately.setter + def capture_immediately(self, value: Optional[bool]) -> None: + self.__capture_immediately = value + + @property + def card_data(self) -> Optional[PlainCardData]: + """ + | Card data in plain text + + Type: :class:`worldline.acquiring.sdk.v1.domain.plain_card_data.PlainCardData` + """ + return self.__card_data + + @card_data.setter + def card_data(self, value: Optional[PlainCardData]) -> None: + self.__card_data = value + + @property + def card_entry_mode(self) -> Optional[str]: + """ + | Card entry mode used in the transaction, defaults to ECOMMERCE + + Type: str + """ + return self.__card_entry_mode + + @card_entry_mode.setter + def card_entry_mode(self, value: Optional[str]) -> None: + self.__card_entry_mode = value + + @property + def network_token_data(self) -> Optional[NetworkTokenData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.network_token_data.NetworkTokenData` + """ + return self.__network_token_data + + @network_token_data.setter + def network_token_data(self, value: Optional[NetworkTokenData]) -> None: + self.__network_token_data = value + + @property + def point_of_sale_data(self) -> Optional[PointOfSaleData]: + """ + | Payment terminal request data + + Type: :class:`worldline.acquiring.sdk.v1.domain.point_of_sale_data.PointOfSaleData` + """ + return self.__point_of_sale_data + + @point_of_sale_data.setter + def point_of_sale_data(self, value: Optional[PointOfSaleData]) -> None: + self.__point_of_sale_data = value + + @property + def wallet_id(self) -> Optional[str]: + """ + | Type of wallet, values are assigned by card schemes, e.g. 101 for MasterPass in eCommerce, 102 for MasterPass NFC, 103 for Apple Pay, 216 for Google Pay and 217 for Samsung Pay + + Type: str + """ + return self.__wallet_id + + @wallet_id.setter + def wallet_id(self, value: Optional[str]) -> None: + self.__wallet_id = value + + def to_dictionary(self) -> dict: + dictionary = super(CardPaymentDataForRefund, self).to_dictionary() + if self.brand is not None: + dictionary['brand'] = self.brand + if self.capture_immediately is not None: + dictionary['captureImmediately'] = self.capture_immediately + if self.card_data is not None: + dictionary['cardData'] = self.card_data.to_dictionary() + if self.card_entry_mode is not None: + dictionary['cardEntryMode'] = self.card_entry_mode + if self.network_token_data is not None: + dictionary['networkTokenData'] = self.network_token_data.to_dictionary() + if self.point_of_sale_data is not None: + dictionary['pointOfSaleData'] = self.point_of_sale_data.to_dictionary() + if self.wallet_id is not None: + dictionary['walletId'] = self.wallet_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardPaymentDataForRefund': + super(CardPaymentDataForRefund, self).from_dictionary(dictionary) + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'captureImmediately' in dictionary: + self.capture_immediately = dictionary['captureImmediately'] + if 'cardData' in dictionary: + if not isinstance(dictionary['cardData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardData'])) + value = PlainCardData() + self.card_data = value.from_dictionary(dictionary['cardData']) + if 'cardEntryMode' in dictionary: + self.card_entry_mode = dictionary['cardEntryMode'] + if 'networkTokenData' in dictionary: + if not isinstance(dictionary['networkTokenData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['networkTokenData'])) + value = NetworkTokenData() + self.network_token_data = value.from_dictionary(dictionary['networkTokenData']) + if 'pointOfSaleData' in dictionary: + if not isinstance(dictionary['pointOfSaleData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['pointOfSaleData'])) + value = PointOfSaleData() + self.point_of_sale_data = value.from_dictionary(dictionary['pointOfSaleData']) + if 'walletId' in dictionary: + self.wallet_id = dictionary['walletId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.py b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.py new file mode 100644 index 0000000..40737fe --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .point_of_sale_data import PointOfSaleData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardPaymentDataForResource(DataObject): + + __brand: Optional[str] = None + __point_of_sale_data: Optional[PointOfSaleData] = None + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def point_of_sale_data(self) -> Optional[PointOfSaleData]: + """ + | Payment terminal request data + + Type: :class:`worldline.acquiring.sdk.v1.domain.point_of_sale_data.PointOfSaleData` + """ + return self.__point_of_sale_data + + @point_of_sale_data.setter + def point_of_sale_data(self, value: Optional[PointOfSaleData]) -> None: + self.__point_of_sale_data = value + + def to_dictionary(self) -> dict: + dictionary = super(CardPaymentDataForResource, self).to_dictionary() + if self.brand is not None: + dictionary['brand'] = self.brand + if self.point_of_sale_data is not None: + dictionary['pointOfSaleData'] = self.point_of_sale_data.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardPaymentDataForResource': + super(CardPaymentDataForResource, self).from_dictionary(dictionary) + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'pointOfSaleData' in dictionary: + if not isinstance(dictionary['pointOfSaleData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['pointOfSaleData'])) + value = PointOfSaleData() + self.point_of_sale_data = value.from_dictionary(dictionary['pointOfSaleData']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.py b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.py new file mode 100644 index 0000000..94fcb65 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .e_commerce_data_for_response import ECommerceDataForResponse +from .point_of_sale_data import PointOfSaleData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardPaymentDataForResponse(DataObject): + + __brand: Optional[str] = None + __ecommerce_data: Optional[ECommerceDataForResponse] = None + __point_of_sale_data: Optional[PointOfSaleData] = None + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def ecommerce_data(self) -> Optional[ECommerceDataForResponse]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.e_commerce_data_for_response.ECommerceDataForResponse` + """ + return self.__ecommerce_data + + @ecommerce_data.setter + def ecommerce_data(self, value: Optional[ECommerceDataForResponse]) -> None: + self.__ecommerce_data = value + + @property + def point_of_sale_data(self) -> Optional[PointOfSaleData]: + """ + | Payment terminal request data + + Type: :class:`worldline.acquiring.sdk.v1.domain.point_of_sale_data.PointOfSaleData` + """ + return self.__point_of_sale_data + + @point_of_sale_data.setter + def point_of_sale_data(self, value: Optional[PointOfSaleData]) -> None: + self.__point_of_sale_data = value + + def to_dictionary(self) -> dict: + dictionary = super(CardPaymentDataForResponse, self).to_dictionary() + if self.brand is not None: + dictionary['brand'] = self.brand + if self.ecommerce_data is not None: + dictionary['ecommerceData'] = self.ecommerce_data.to_dictionary() + if self.point_of_sale_data is not None: + dictionary['pointOfSaleData'] = self.point_of_sale_data.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardPaymentDataForResponse': + super(CardPaymentDataForResponse, self).from_dictionary(dictionary) + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'ecommerceData' in dictionary: + if not isinstance(dictionary['ecommerceData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['ecommerceData'])) + value = ECommerceDataForResponse() + self.ecommerce_data = value.from_dictionary(dictionary['ecommerceData']) + if 'pointOfSaleData' in dictionary: + if not isinstance(dictionary['pointOfSaleData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['pointOfSaleData'])) + value = PointOfSaleData() + self.point_of_sale_data = value.from_dictionary(dictionary['pointOfSaleData']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.py b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.py new file mode 100644 index 0000000..8454bf8 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .card_on_file_data import CardOnFileData +from .e_commerce_data_for_account_verification import ECommerceDataForAccountVerification +from .network_token_data import NetworkTokenData +from .plain_card_data import PlainCardData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class CardPaymentDataForVerification(DataObject): + + __brand: Optional[str] = None + __card_data: Optional[PlainCardData] = None + __card_entry_mode: Optional[str] = None + __card_on_file_data: Optional[CardOnFileData] = None + __cardholder_verification_method: Optional[str] = None + __ecommerce_data: Optional[ECommerceDataForAccountVerification] = None + __network_token_data: Optional[NetworkTokenData] = None + __wallet_id: Optional[str] = None + + @property + def brand(self) -> Optional[str]: + """ + | The card brand + + Type: str + """ + return self.__brand + + @brand.setter + def brand(self, value: Optional[str]) -> None: + self.__brand = value + + @property + def card_data(self) -> Optional[PlainCardData]: + """ + | Card data in plain text + + Type: :class:`worldline.acquiring.sdk.v1.domain.plain_card_data.PlainCardData` + """ + return self.__card_data + + @card_data.setter + def card_data(self, value: Optional[PlainCardData]) -> None: + self.__card_data = value + + @property + def card_entry_mode(self) -> Optional[str]: + """ + | Card entry mode used in the transaction, defaults to ECOMMERCE + + Type: str + """ + return self.__card_entry_mode + + @card_entry_mode.setter + def card_entry_mode(self, value: Optional[str]) -> None: + self.__card_entry_mode = value + + @property + def card_on_file_data(self) -> Optional[CardOnFileData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_on_file_data.CardOnFileData` + """ + return self.__card_on_file_data + + @card_on_file_data.setter + def card_on_file_data(self, value: Optional[CardOnFileData]) -> None: + self.__card_on_file_data = value + + @property + def cardholder_verification_method(self) -> Optional[str]: + """ + | Cardholder verification method used in the transaction + + Type: str + """ + return self.__cardholder_verification_method + + @cardholder_verification_method.setter + def cardholder_verification_method(self, value: Optional[str]) -> None: + self.__cardholder_verification_method = value + + @property + def ecommerce_data(self) -> Optional[ECommerceDataForAccountVerification]: + """ + | Request data for eCommerce and MOTO transactions + + Type: :class:`worldline.acquiring.sdk.v1.domain.e_commerce_data_for_account_verification.ECommerceDataForAccountVerification` + """ + return self.__ecommerce_data + + @ecommerce_data.setter + def ecommerce_data(self, value: Optional[ECommerceDataForAccountVerification]) -> None: + self.__ecommerce_data = value + + @property + def network_token_data(self) -> Optional[NetworkTokenData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.network_token_data.NetworkTokenData` + """ + return self.__network_token_data + + @network_token_data.setter + def network_token_data(self, value: Optional[NetworkTokenData]) -> None: + self.__network_token_data = value + + @property + def wallet_id(self) -> Optional[str]: + """ + | Type of wallet, values are assigned by card schemes, e.g. 101 for MasterPass in eCommerce, 102 for MasterPass NFC, 103 for Apple Pay, 216 for Google Pay and 217 for Samsung Pay + + Type: str + """ + return self.__wallet_id + + @wallet_id.setter + def wallet_id(self, value: Optional[str]) -> None: + self.__wallet_id = value + + def to_dictionary(self) -> dict: + dictionary = super(CardPaymentDataForVerification, self).to_dictionary() + if self.brand is not None: + dictionary['brand'] = self.brand + if self.card_data is not None: + dictionary['cardData'] = self.card_data.to_dictionary() + if self.card_entry_mode is not None: + dictionary['cardEntryMode'] = self.card_entry_mode + if self.card_on_file_data is not None: + dictionary['cardOnFileData'] = self.card_on_file_data.to_dictionary() + if self.cardholder_verification_method is not None: + dictionary['cardholderVerificationMethod'] = self.cardholder_verification_method + if self.ecommerce_data is not None: + dictionary['ecommerceData'] = self.ecommerce_data.to_dictionary() + if self.network_token_data is not None: + dictionary['networkTokenData'] = self.network_token_data.to_dictionary() + if self.wallet_id is not None: + dictionary['walletId'] = self.wallet_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'CardPaymentDataForVerification': + super(CardPaymentDataForVerification, self).from_dictionary(dictionary) + if 'brand' in dictionary: + self.brand = dictionary['brand'] + if 'cardData' in dictionary: + if not isinstance(dictionary['cardData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardData'])) + value = PlainCardData() + self.card_data = value.from_dictionary(dictionary['cardData']) + if 'cardEntryMode' in dictionary: + self.card_entry_mode = dictionary['cardEntryMode'] + if 'cardOnFileData' in dictionary: + if not isinstance(dictionary['cardOnFileData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardOnFileData'])) + value = CardOnFileData() + self.card_on_file_data = value.from_dictionary(dictionary['cardOnFileData']) + if 'cardholderVerificationMethod' in dictionary: + self.cardholder_verification_method = dictionary['cardholderVerificationMethod'] + if 'ecommerceData' in dictionary: + if not isinstance(dictionary['ecommerceData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['ecommerceData'])) + value = ECommerceDataForAccountVerification() + self.ecommerce_data = value.from_dictionary(dictionary['ecommerceData']) + if 'networkTokenData' in dictionary: + if not isinstance(dictionary['networkTokenData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['networkTokenData'])) + value = NetworkTokenData() + self.network_token_data = value.from_dictionary(dictionary['networkTokenData']) + if 'walletId' in dictionary: + self.wallet_id = dictionary['walletId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/dcc_data.py b/worldline/acquiring/sdk/v1/domain/dcc_data.py new file mode 100644 index 0000000..f383981 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/dcc_data.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class DccData(DataObject): + + __amount: Optional[int] = None + __conversion_rate: Optional[float] = None + __currency_code: Optional[str] = None + __number_of_decimals: Optional[int] = None + + @property + def amount(self) -> Optional[int]: + """ + | Amount of transaction formatted according to card scheme specifications. E.g. 100 for 1.00 EUR. Either this or amount must be present. + + Type: int + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[int]) -> None: + self.__amount = value + + @property + def conversion_rate(self) -> Optional[float]: + """ + | Currency conversion rate in decimal notation. + | Either this or isoConversionRate must be present + + Type: float + """ + return self.__conversion_rate + + @conversion_rate.setter + def conversion_rate(self, value: Optional[float]) -> None: + self.__conversion_rate = value + + @property + def currency_code(self) -> Optional[str]: + """ + | Alpha-numeric ISO 4217 currency code for transaction, e.g. EUR + + Type: str + """ + return self.__currency_code + + @currency_code.setter + def currency_code(self, value: Optional[str]) -> None: + self.__currency_code = value + + @property + def number_of_decimals(self) -> Optional[int]: + """ + | Number of decimals in the amount + + Type: int + """ + return self.__number_of_decimals + + @number_of_decimals.setter + def number_of_decimals(self, value: Optional[int]) -> None: + self.__number_of_decimals = value + + def to_dictionary(self) -> dict: + dictionary = super(DccData, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount + if self.conversion_rate is not None: + dictionary['conversionRate'] = self.conversion_rate + if self.currency_code is not None: + dictionary['currencyCode'] = self.currency_code + if self.number_of_decimals is not None: + dictionary['numberOfDecimals'] = self.number_of_decimals + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'DccData': + super(DccData, self).from_dictionary(dictionary) + if 'amount' in dictionary: + self.amount = dictionary['amount'] + if 'conversionRate' in dictionary: + self.conversion_rate = dictionary['conversionRate'] + if 'currencyCode' in dictionary: + self.currency_code = dictionary['currencyCode'] + if 'numberOfDecimals' in dictionary: + self.number_of_decimals = dictionary['numberOfDecimals'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/dcc_proposal.py b/worldline/acquiring/sdk/v1/domain/dcc_proposal.py new file mode 100644 index 0000000..45aa871 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/dcc_proposal.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .amount_data import AmountData +from .rate_data import RateData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class DccProposal(DataObject): + + __original_amount: Optional[AmountData] = None + __rate: Optional[RateData] = None + __rate_reference_id: Optional[str] = None + __resulting_amount: Optional[AmountData] = None + + @property + def original_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__original_amount + + @original_amount.setter + def original_amount(self, value: Optional[AmountData]) -> None: + self.__original_amount = value + + @property + def rate(self) -> Optional[RateData]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.rate_data.RateData` + """ + return self.__rate + + @rate.setter + def rate(self, value: Optional[RateData]) -> None: + self.__rate = value + + @property + def rate_reference_id(self) -> Optional[str]: + """ + | The rate reference ID + + Type: str + """ + return self.__rate_reference_id + + @rate_reference_id.setter + def rate_reference_id(self, value: Optional[str]) -> None: + self.__rate_reference_id = value + + @property + def resulting_amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__resulting_amount + + @resulting_amount.setter + def resulting_amount(self, value: Optional[AmountData]) -> None: + self.__resulting_amount = value + + def to_dictionary(self) -> dict: + dictionary = super(DccProposal, self).to_dictionary() + if self.original_amount is not None: + dictionary['originalAmount'] = self.original_amount.to_dictionary() + if self.rate is not None: + dictionary['rate'] = self.rate.to_dictionary() + if self.rate_reference_id is not None: + dictionary['rateReferenceId'] = self.rate_reference_id + if self.resulting_amount is not None: + dictionary['resultingAmount'] = self.resulting_amount.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'DccProposal': + super(DccProposal, self).from_dictionary(dictionary) + if 'originalAmount' in dictionary: + if not isinstance(dictionary['originalAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['originalAmount'])) + value = AmountData() + self.original_amount = value.from_dictionary(dictionary['originalAmount']) + if 'rate' in dictionary: + if not isinstance(dictionary['rate'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['rate'])) + value = RateData() + self.rate = value.from_dictionary(dictionary['rate']) + if 'rateReferenceId' in dictionary: + self.rate_reference_id = dictionary['rateReferenceId'] + if 'resultingAmount' in dictionary: + if not isinstance(dictionary['resultingAmount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['resultingAmount'])) + value = AmountData() + self.resulting_amount = value.from_dictionary(dictionary['resultingAmount']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/e_commerce_data.py b/worldline/acquiring/sdk/v1/domain/e_commerce_data.py new file mode 100644 index 0000000..d0485b0 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/e_commerce_data.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .address_verification_data import AddressVerificationData +from .three_d_secure import ThreeDSecure + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ECommerceData(DataObject): + + __address_verification_data: Optional[AddressVerificationData] = None + __sca_exemption_request: Optional[str] = None + __three_d_secure: Optional[ThreeDSecure] = None + + @property + def address_verification_data(self) -> Optional[AddressVerificationData]: + """ + | Address Verification System data + + Type: :class:`worldline.acquiring.sdk.v1.domain.address_verification_data.AddressVerificationData` + """ + return self.__address_verification_data + + @address_verification_data.setter + def address_verification_data(self, value: Optional[AddressVerificationData]) -> None: + self.__address_verification_data = value + + @property + def sca_exemption_request(self) -> Optional[str]: + """ + | Strong customer authentication exemption request + + Type: str + """ + return self.__sca_exemption_request + + @sca_exemption_request.setter + def sca_exemption_request(self, value: Optional[str]) -> None: + self.__sca_exemption_request = value + + @property + def three_d_secure(self) -> Optional[ThreeDSecure]: + """ + | 3D Secure data. + | Please note that if AAV or CAVV or equivalent is missing, transaction should not be flagged as 3D Secure. + + Type: :class:`worldline.acquiring.sdk.v1.domain.three_d_secure.ThreeDSecure` + """ + return self.__three_d_secure + + @three_d_secure.setter + def three_d_secure(self, value: Optional[ThreeDSecure]) -> None: + self.__three_d_secure = value + + def to_dictionary(self) -> dict: + dictionary = super(ECommerceData, self).to_dictionary() + if self.address_verification_data is not None: + dictionary['addressVerificationData'] = self.address_verification_data.to_dictionary() + if self.sca_exemption_request is not None: + dictionary['scaExemptionRequest'] = self.sca_exemption_request + if self.three_d_secure is not None: + dictionary['threeDSecure'] = self.three_d_secure.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ECommerceData': + super(ECommerceData, self).from_dictionary(dictionary) + if 'addressVerificationData' in dictionary: + if not isinstance(dictionary['addressVerificationData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['addressVerificationData'])) + value = AddressVerificationData() + self.address_verification_data = value.from_dictionary(dictionary['addressVerificationData']) + if 'scaExemptionRequest' in dictionary: + self.sca_exemption_request = dictionary['scaExemptionRequest'] + if 'threeDSecure' in dictionary: + if not isinstance(dictionary['threeDSecure'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['threeDSecure'])) + value = ThreeDSecure() + self.three_d_secure = value.from_dictionary(dictionary['threeDSecure']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.py b/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.py new file mode 100644 index 0000000..8acb147 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .address_verification_data import AddressVerificationData +from .three_d_secure import ThreeDSecure + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ECommerceDataForAccountVerification(DataObject): + + __address_verification_data: Optional[AddressVerificationData] = None + __three_d_secure: Optional[ThreeDSecure] = None + + @property + def address_verification_data(self) -> Optional[AddressVerificationData]: + """ + | Address Verification System data + + Type: :class:`worldline.acquiring.sdk.v1.domain.address_verification_data.AddressVerificationData` + """ + return self.__address_verification_data + + @address_verification_data.setter + def address_verification_data(self, value: Optional[AddressVerificationData]) -> None: + self.__address_verification_data = value + + @property + def three_d_secure(self) -> Optional[ThreeDSecure]: + """ + | 3D Secure data. + | Please note that if AAV or CAVV or equivalent is missing, transaction should not be flagged as 3D Secure. + + Type: :class:`worldline.acquiring.sdk.v1.domain.three_d_secure.ThreeDSecure` + """ + return self.__three_d_secure + + @three_d_secure.setter + def three_d_secure(self, value: Optional[ThreeDSecure]) -> None: + self.__three_d_secure = value + + def to_dictionary(self) -> dict: + dictionary = super(ECommerceDataForAccountVerification, self).to_dictionary() + if self.address_verification_data is not None: + dictionary['addressVerificationData'] = self.address_verification_data.to_dictionary() + if self.three_d_secure is not None: + dictionary['threeDSecure'] = self.three_d_secure.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ECommerceDataForAccountVerification': + super(ECommerceDataForAccountVerification, self).from_dictionary(dictionary) + if 'addressVerificationData' in dictionary: + if not isinstance(dictionary['addressVerificationData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['addressVerificationData'])) + value = AddressVerificationData() + self.address_verification_data = value.from_dictionary(dictionary['addressVerificationData']) + if 'threeDSecure' in dictionary: + if not isinstance(dictionary['threeDSecure'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['threeDSecure'])) + value = ThreeDSecure() + self.three_d_secure = value.from_dictionary(dictionary['threeDSecure']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.py b/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.py new file mode 100644 index 0000000..2a6ad2c --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ECommerceDataForResponse(DataObject): + + __address_verification_result: Optional[str] = None + __card_security_code_result: Optional[str] = None + + @property + def address_verification_result(self) -> Optional[str]: + """ + | Result of Address Verification Result + + Type: str + """ + return self.__address_verification_result + + @address_verification_result.setter + def address_verification_result(self, value: Optional[str]) -> None: + self.__address_verification_result = value + + @property + def card_security_code_result(self) -> Optional[str]: + """ + | Result of card security code check + + Type: str + """ + return self.__card_security_code_result + + @card_security_code_result.setter + def card_security_code_result(self, value: Optional[str]) -> None: + self.__card_security_code_result = value + + def to_dictionary(self) -> dict: + dictionary = super(ECommerceDataForResponse, self).to_dictionary() + if self.address_verification_result is not None: + dictionary['addressVerificationResult'] = self.address_verification_result + if self.card_security_code_result is not None: + dictionary['cardSecurityCodeResult'] = self.card_security_code_result + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ECommerceDataForResponse': + super(ECommerceDataForResponse, self).from_dictionary(dictionary) + if 'addressVerificationResult' in dictionary: + self.address_verification_result = dictionary['addressVerificationResult'] + if 'cardSecurityCodeResult' in dictionary: + self.card_security_code_result = dictionary['cardSecurityCodeResult'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.py b/worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.py new file mode 100644 index 0000000..9cb88f6 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .card_data_for_dcc import CardDataForDcc +from .point_of_sale_data_for_dcc import PointOfSaleDataForDcc +from .transaction_data_for_dcc import TransactionDataForDcc + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class GetDCCRateRequest(DataObject): + + __card_payment_data: Optional[CardDataForDcc] = None + __operation_id: Optional[str] = None + __point_of_sale_data: Optional[PointOfSaleDataForDcc] = None + __rate_reference_id: Optional[str] = None + __target_currency: Optional[str] = None + __transaction: Optional[TransactionDataForDcc] = None + + @property + def card_payment_data(self) -> Optional[CardDataForDcc]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.card_data_for_dcc.CardDataForDcc` + """ + return self.__card_payment_data + + @card_payment_data.setter + def card_payment_data(self, value: Optional[CardDataForDcc]) -> None: + self.__card_payment_data = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A unique identifier of the operation, generated by the client. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def point_of_sale_data(self) -> Optional[PointOfSaleDataForDcc]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.point_of_sale_data_for_dcc.PointOfSaleDataForDcc` + """ + return self.__point_of_sale_data + + @point_of_sale_data.setter + def point_of_sale_data(self, value: Optional[PointOfSaleDataForDcc]) -> None: + self.__point_of_sale_data = value + + @property + def rate_reference_id(self) -> Optional[str]: + """ + | The reference of a previously used rate + | This can be used in case of refund if you want to use the same rate as the original transaction. + + Type: str + """ + return self.__rate_reference_id + + @rate_reference_id.setter + def rate_reference_id(self, value: Optional[str]) -> None: + self.__rate_reference_id = value + + @property + def target_currency(self) -> Optional[str]: + """ + | The currency to convert to + + Type: str + """ + return self.__target_currency + + @target_currency.setter + def target_currency(self, value: Optional[str]) -> None: + self.__target_currency = value + + @property + def transaction(self) -> Optional[TransactionDataForDcc]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.transaction_data_for_dcc.TransactionDataForDcc` + """ + return self.__transaction + + @transaction.setter + def transaction(self, value: Optional[TransactionDataForDcc]) -> None: + self.__transaction = value + + def to_dictionary(self) -> dict: + dictionary = super(GetDCCRateRequest, self).to_dictionary() + if self.card_payment_data is not None: + dictionary['cardPaymentData'] = self.card_payment_data.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.point_of_sale_data is not None: + dictionary['pointOfSaleData'] = self.point_of_sale_data.to_dictionary() + if self.rate_reference_id is not None: + dictionary['rateReferenceId'] = self.rate_reference_id + if self.target_currency is not None: + dictionary['targetCurrency'] = self.target_currency + if self.transaction is not None: + dictionary['transaction'] = self.transaction.to_dictionary() + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'GetDCCRateRequest': + super(GetDCCRateRequest, self).from_dictionary(dictionary) + if 'cardPaymentData' in dictionary: + if not isinstance(dictionary['cardPaymentData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['cardPaymentData'])) + value = CardDataForDcc() + self.card_payment_data = value.from_dictionary(dictionary['cardPaymentData']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'pointOfSaleData' in dictionary: + if not isinstance(dictionary['pointOfSaleData'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['pointOfSaleData'])) + value = PointOfSaleDataForDcc() + self.point_of_sale_data = value.from_dictionary(dictionary['pointOfSaleData']) + if 'rateReferenceId' in dictionary: + self.rate_reference_id = dictionary['rateReferenceId'] + if 'targetCurrency' in dictionary: + self.target_currency = dictionary['targetCurrency'] + if 'transaction' in dictionary: + if not isinstance(dictionary['transaction'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['transaction'])) + value = TransactionDataForDcc() + self.transaction = value.from_dictionary(dictionary['transaction']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.py b/worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.py new file mode 100644 index 0000000..8da3769 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .dcc_proposal import DccProposal + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class GetDccRateResponse(DataObject): + + __disclaimer_display: Optional[str] = None + __disclaimer_receipt: Optional[str] = None + __proposal: Optional[DccProposal] = None + __result: Optional[str] = None + + @property + def disclaimer_display(self) -> Optional[str]: + """ + | The disclaimer display + + Type: str + """ + return self.__disclaimer_display + + @disclaimer_display.setter + def disclaimer_display(self, value: Optional[str]) -> None: + self.__disclaimer_display = value + + @property + def disclaimer_receipt(self) -> Optional[str]: + """ + | The disclaimer receipt + + Type: str + """ + return self.__disclaimer_receipt + + @disclaimer_receipt.setter + def disclaimer_receipt(self, value: Optional[str]) -> None: + self.__disclaimer_receipt = value + + @property + def proposal(self) -> Optional[DccProposal]: + """ + Type: :class:`worldline.acquiring.sdk.v1.domain.dcc_proposal.DccProposal` + """ + return self.__proposal + + @proposal.setter + def proposal(self, value: Optional[DccProposal]) -> None: + self.__proposal = value + + @property + def result(self) -> Optional[str]: + """ + | The result of the operation + + Type: str + """ + return self.__result + + @result.setter + def result(self, value: Optional[str]) -> None: + self.__result = value + + def to_dictionary(self) -> dict: + dictionary = super(GetDccRateResponse, self).to_dictionary() + if self.disclaimer_display is not None: + dictionary['disclaimerDisplay'] = self.disclaimer_display + if self.disclaimer_receipt is not None: + dictionary['disclaimerReceipt'] = self.disclaimer_receipt + if self.proposal is not None: + dictionary['proposal'] = self.proposal.to_dictionary() + if self.result is not None: + dictionary['result'] = self.result + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'GetDccRateResponse': + super(GetDccRateResponse, self).from_dictionary(dictionary) + if 'disclaimerDisplay' in dictionary: + self.disclaimer_display = dictionary['disclaimerDisplay'] + if 'disclaimerReceipt' in dictionary: + self.disclaimer_receipt = dictionary['disclaimerReceipt'] + if 'proposal' in dictionary: + if not isinstance(dictionary['proposal'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['proposal'])) + value = DccProposal() + self.proposal = value.from_dictionary(dictionary['proposal']) + if 'result' in dictionary: + self.result = dictionary['result'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.py b/worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.py new file mode 100644 index 0000000..0245f41 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class InitialCardOnFileData(DataObject): + + __future_use: Optional[str] = None + __transaction_type: Optional[str] = None + + @property + def future_use(self) -> Optional[str]: + """ + | Future use + + Type: str + """ + return self.__future_use + + @future_use.setter + def future_use(self, value: Optional[str]) -> None: + self.__future_use = value + + @property + def transaction_type(self) -> Optional[str]: + """ + | Transaction type + + Type: str + """ + return self.__transaction_type + + @transaction_type.setter + def transaction_type(self, value: Optional[str]) -> None: + self.__transaction_type = value + + def to_dictionary(self) -> dict: + dictionary = super(InitialCardOnFileData, self).to_dictionary() + if self.future_use is not None: + dictionary['futureUse'] = self.future_use + if self.transaction_type is not None: + dictionary['transactionType'] = self.transaction_type + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'InitialCardOnFileData': + super(InitialCardOnFileData, self).from_dictionary(dictionary) + if 'futureUse' in dictionary: + self.future_use = dictionary['futureUse'] + if 'transactionType' in dictionary: + self.transaction_type = dictionary['transactionType'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/merchant_data.py b/worldline/acquiring/sdk/v1/domain/merchant_data.py new file mode 100644 index 0000000..e720749 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/merchant_data.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class MerchantData(DataObject): + + __address: Optional[str] = None + __city: Optional[str] = None + __country_code: Optional[str] = None + __merchant_category_code: Optional[int] = None + __name: Optional[str] = None + __postal_code: Optional[str] = None + __state_code: Optional[str] = None + + @property + def address(self) -> Optional[str]: + """ + | Street address + + Type: str + """ + return self.__address + + @address.setter + def address(self, value: Optional[str]) -> None: + self.__address = value + + @property + def city(self) -> Optional[str]: + """ + | Address city + + Type: str + """ + return self.__city + + @city.setter + def city(self, value: Optional[str]) -> None: + self.__city = value + + @property + def country_code(self) -> Optional[str]: + """ + | Address country code, ISO 3166 international standard + + Type: str + """ + return self.__country_code + + @country_code.setter + def country_code(self, value: Optional[str]) -> None: + self.__country_code = value + + @property + def merchant_category_code(self) -> Optional[int]: + """ + | Merchant category code (MCC) + + Type: int + """ + return self.__merchant_category_code + + @merchant_category_code.setter + def merchant_category_code(self, value: Optional[int]) -> None: + self.__merchant_category_code = value + + @property + def name(self) -> Optional[str]: + """ + | Merchant name + + Type: str + """ + return self.__name + + @name.setter + def name(self, value: Optional[str]) -> None: + self.__name = value + + @property + def postal_code(self) -> Optional[str]: + """ + | Address postal code + + Type: str + """ + return self.__postal_code + + @postal_code.setter + def postal_code(self, value: Optional[str]) -> None: + self.__postal_code = value + + @property + def state_code(self) -> Optional[str]: + """ + | Address state code, only supplied if country is US or CA + + Type: str + """ + return self.__state_code + + @state_code.setter + def state_code(self, value: Optional[str]) -> None: + self.__state_code = value + + def to_dictionary(self) -> dict: + dictionary = super(MerchantData, self).to_dictionary() + if self.address is not None: + dictionary['address'] = self.address + if self.city is not None: + dictionary['city'] = self.city + if self.country_code is not None: + dictionary['countryCode'] = self.country_code + if self.merchant_category_code is not None: + dictionary['merchantCategoryCode'] = self.merchant_category_code + if self.name is not None: + dictionary['name'] = self.name + if self.postal_code is not None: + dictionary['postalCode'] = self.postal_code + if self.state_code is not None: + dictionary['stateCode'] = self.state_code + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'MerchantData': + super(MerchantData, self).from_dictionary(dictionary) + if 'address' in dictionary: + self.address = dictionary['address'] + if 'city' in dictionary: + self.city = dictionary['city'] + if 'countryCode' in dictionary: + self.country_code = dictionary['countryCode'] + if 'merchantCategoryCode' in dictionary: + self.merchant_category_code = dictionary['merchantCategoryCode'] + if 'name' in dictionary: + self.name = dictionary['name'] + if 'postalCode' in dictionary: + self.postal_code = dictionary['postalCode'] + if 'stateCode' in dictionary: + self.state_code = dictionary['stateCode'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/network_token_data.py b/worldline/acquiring/sdk/v1/domain/network_token_data.py new file mode 100644 index 0000000..ff5e1ad --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/network_token_data.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class NetworkTokenData(DataObject): + + __cryptogram: Optional[str] = None + __eci: Optional[str] = None + + @property + def cryptogram(self) -> Optional[str]: + """ + | Network token cryptogram + + Type: str + """ + return self.__cryptogram + + @cryptogram.setter + def cryptogram(self, value: Optional[str]) -> None: + self.__cryptogram = value + + @property + def eci(self) -> Optional[str]: + """ + | Electronic Commerce Indicator + | Value returned by the 3D Secure process that indicates the level of authentication. + | Contains different values depending on the brand. + + Type: str + """ + return self.__eci + + @eci.setter + def eci(self, value: Optional[str]) -> None: + self.__eci = value + + def to_dictionary(self) -> dict: + dictionary = super(NetworkTokenData, self).to_dictionary() + if self.cryptogram is not None: + dictionary['cryptogram'] = self.cryptogram + if self.eci is not None: + dictionary['eci'] = self.eci + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'NetworkTokenData': + super(NetworkTokenData, self).from_dictionary(dictionary) + if 'cryptogram' in dictionary: + self.cryptogram = dictionary['cryptogram'] + if 'eci' in dictionary: + self.eci = dictionary['eci'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/payment_references.py b/worldline/acquiring/sdk/v1/domain/payment_references.py new file mode 100644 index 0000000..1235272 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/payment_references.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class PaymentReferences(DataObject): + + __dynamic_descriptor: Optional[str] = None + __merchant_reference: Optional[str] = None + __retrieval_reference_number: Optional[str] = None + + @property + def dynamic_descriptor(self) -> Optional[str]: + """ + | Dynamic descriptor gives you the ability to control the descriptor on the credit card statement of the customer. + + Type: str + """ + return self.__dynamic_descriptor + + @dynamic_descriptor.setter + def dynamic_descriptor(self, value: Optional[str]) -> None: + self.__dynamic_descriptor = value + + @property + def merchant_reference(self) -> Optional[str]: + """ + | Reference for the transaction to allow the merchant to reconcile their payments in our report files. + | It is advised to submit a unique value per transaction. + | The value provided here is returned in the baseTrxType/addlMercData element of the MRX file. + + Type: str + """ + return self.__merchant_reference + + @merchant_reference.setter + def merchant_reference(self, value: Optional[str]) -> None: + self.__merchant_reference = value + + @property + def retrieval_reference_number(self) -> Optional[str]: + """ + | Retrieval reference number for transaction, must be AN(12) if provided + + Type: str + """ + return self.__retrieval_reference_number + + @retrieval_reference_number.setter + def retrieval_reference_number(self, value: Optional[str]) -> None: + self.__retrieval_reference_number = value + + def to_dictionary(self) -> dict: + dictionary = super(PaymentReferences, self).to_dictionary() + if self.dynamic_descriptor is not None: + dictionary['dynamicDescriptor'] = self.dynamic_descriptor + if self.merchant_reference is not None: + dictionary['merchantReference'] = self.merchant_reference + if self.retrieval_reference_number is not None: + dictionary['retrievalReferenceNumber'] = self.retrieval_reference_number + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'PaymentReferences': + super(PaymentReferences, self).from_dictionary(dictionary) + if 'dynamicDescriptor' in dictionary: + self.dynamic_descriptor = dictionary['dynamicDescriptor'] + if 'merchantReference' in dictionary: + self.merchant_reference = dictionary['merchantReference'] + if 'retrievalReferenceNumber' in dictionary: + self.retrieval_reference_number = dictionary['retrievalReferenceNumber'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/plain_card_data.py b/worldline/acquiring/sdk/v1/domain/plain_card_data.py new file mode 100644 index 0000000..14a8a09 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/plain_card_data.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class PlainCardData(DataObject): + + __card_number: Optional[str] = None + __card_security_code: Optional[str] = None + __expiry_date: Optional[str] = None + + @property + def card_number(self) -> Optional[str]: + """ + | Card number (PAN, network token or DPAN). + + Type: str + """ + return self.__card_number + + @card_number.setter + def card_number(self, value: Optional[str]) -> None: + self.__card_number = value + + @property + def card_security_code(self) -> Optional[str]: + """ + | The security code indicated on the card + | Based on the card brand, it can be 3 or 4 digits long + | and have different names: CVV2, CVC2, CVN2, CID, CVC, CAV2, etc. + + Type: str + """ + return self.__card_security_code + + @card_security_code.setter + def card_security_code(self, value: Optional[str]) -> None: + self.__card_security_code = value + + @property + def expiry_date(self) -> Optional[str]: + """ + | Card or token expiry date in format MMYYYY + + Type: str + """ + return self.__expiry_date + + @expiry_date.setter + def expiry_date(self, value: Optional[str]) -> None: + self.__expiry_date = value + + def to_dictionary(self) -> dict: + dictionary = super(PlainCardData, self).to_dictionary() + if self.card_number is not None: + dictionary['cardNumber'] = self.card_number + if self.card_security_code is not None: + dictionary['cardSecurityCode'] = self.card_security_code + if self.expiry_date is not None: + dictionary['expiryDate'] = self.expiry_date + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'PlainCardData': + super(PlainCardData, self).from_dictionary(dictionary) + if 'cardNumber' in dictionary: + self.card_number = dictionary['cardNumber'] + if 'cardSecurityCode' in dictionary: + self.card_security_code = dictionary['cardSecurityCode'] + if 'expiryDate' in dictionary: + self.expiry_date = dictionary['expiryDate'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/point_of_sale_data.py b/worldline/acquiring/sdk/v1/domain/point_of_sale_data.py new file mode 100644 index 0000000..e94ccc1 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/point_of_sale_data.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class PointOfSaleData(DataObject): + + __terminal_id: Optional[str] = None + + @property + def terminal_id(self) -> Optional[str]: + """ + | Terminal ID ANS(8) + + Type: str + """ + return self.__terminal_id + + @terminal_id.setter + def terminal_id(self, value: Optional[str]) -> None: + self.__terminal_id = value + + def to_dictionary(self) -> dict: + dictionary = super(PointOfSaleData, self).to_dictionary() + if self.terminal_id is not None: + dictionary['terminalId'] = self.terminal_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'PointOfSaleData': + super(PointOfSaleData, self).from_dictionary(dictionary) + if 'terminalId' in dictionary: + self.terminal_id = dictionary['terminalId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.py b/worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.py new file mode 100644 index 0000000..07b0010 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class PointOfSaleDataForDcc(DataObject): + + __terminal_country_code: Optional[str] = None + __terminal_id: Optional[str] = None + + @property + def terminal_country_code(self) -> Optional[str]: + """ + | Country code of the terminal + + Type: str + """ + return self.__terminal_country_code + + @terminal_country_code.setter + def terminal_country_code(self, value: Optional[str]) -> None: + self.__terminal_country_code = value + + @property + def terminal_id(self) -> Optional[str]: + """ + | The terminal ID + + Type: str + """ + return self.__terminal_id + + @terminal_id.setter + def terminal_id(self, value: Optional[str]) -> None: + self.__terminal_id = value + + def to_dictionary(self) -> dict: + dictionary = super(PointOfSaleDataForDcc, self).to_dictionary() + if self.terminal_country_code is not None: + dictionary['terminalCountryCode'] = self.terminal_country_code + if self.terminal_id is not None: + dictionary['terminalId'] = self.terminal_id + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'PointOfSaleDataForDcc': + super(PointOfSaleDataForDcc, self).from_dictionary(dictionary) + if 'terminalCountryCode' in dictionary: + self.terminal_country_code = dictionary['terminalCountryCode'] + if 'terminalId' in dictionary: + self.terminal_id = dictionary['terminalId'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/rate_data.py b/worldline/acquiring/sdk/v1/domain/rate_data.py new file mode 100644 index 0000000..7d10477 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/rate_data.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class RateData(DataObject): + + __exchange_rate: Optional[float] = None + __inverted_exchange_rate: Optional[float] = None + __mark_up: Optional[float] = None + __mark_up_basis: Optional[str] = None + __quotation_date_time: Optional[datetime] = None + + @property + def exchange_rate(self) -> Optional[float]: + """ + | The exchange rate + + Type: float + """ + return self.__exchange_rate + + @exchange_rate.setter + def exchange_rate(self, value: Optional[float]) -> None: + self.__exchange_rate = value + + @property + def inverted_exchange_rate(self) -> Optional[float]: + """ + | The inverted exchange rate + + Type: float + """ + return self.__inverted_exchange_rate + + @inverted_exchange_rate.setter + def inverted_exchange_rate(self, value: Optional[float]) -> None: + self.__inverted_exchange_rate = value + + @property + def mark_up(self) -> Optional[float]: + """ + | The mark up applied on the rate (in percentage). + + Type: float + """ + return self.__mark_up + + @mark_up.setter + def mark_up(self, value: Optional[float]) -> None: + self.__mark_up = value + + @property + def mark_up_basis(self) -> Optional[str]: + """ + | The source of the rate the markup is based upon. If the cardholder and the merchant are based in Europe, the mark up is calculated based on the rates provided by the European Central Bank. + + Type: str + """ + return self.__mark_up_basis + + @mark_up_basis.setter + def mark_up_basis(self, value: Optional[str]) -> None: + self.__mark_up_basis = value + + @property + def quotation_date_time(self) -> Optional[datetime]: + """ + | The date and time of the quotation + + Type: datetime + """ + return self.__quotation_date_time + + @quotation_date_time.setter + def quotation_date_time(self, value: Optional[datetime]) -> None: + self.__quotation_date_time = value + + def to_dictionary(self) -> dict: + dictionary = super(RateData, self).to_dictionary() + if self.exchange_rate is not None: + dictionary['exchangeRate'] = self.exchange_rate + if self.inverted_exchange_rate is not None: + dictionary['invertedExchangeRate'] = self.inverted_exchange_rate + if self.mark_up is not None: + dictionary['markUp'] = self.mark_up + if self.mark_up_basis is not None: + dictionary['markUpBasis'] = self.mark_up_basis + if self.quotation_date_time is not None: + dictionary['quotationDateTime'] = DataObject.format_datetime(self.quotation_date_time) + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'RateData': + super(RateData, self).from_dictionary(dictionary) + if 'exchangeRate' in dictionary: + self.exchange_rate = dictionary['exchangeRate'] + if 'invertedExchangeRate' in dictionary: + self.inverted_exchange_rate = dictionary['invertedExchangeRate'] + if 'markUp' in dictionary: + self.mark_up = dictionary['markUp'] + if 'markUpBasis' in dictionary: + self.mark_up_basis = dictionary['markUpBasis'] + if 'quotationDateTime' in dictionary: + self.quotation_date_time = DataObject.parse_datetime(dictionary['quotationDateTime']) + return self diff --git a/worldline/acquiring/sdk/v1/domain/sub_operation.py b/worldline/acquiring/sdk/v1/domain/sub_operation.py new file mode 100644 index 0000000..4e3e65c --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/sub_operation.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class SubOperation(DataObject): + + __amount: Optional[AmountData] = None + __authorization_code: Optional[str] = None + __operation_id: Optional[str] = None + __operation_timestamp: Optional[datetime] = None + __operation_type: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + __retry_after: Optional[str] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def authorization_code(self) -> Optional[str]: + """ + | Authorization approval code + + Type: str + """ + return self.__authorization_code + + @authorization_code.setter + def authorization_code(self, value: Optional[str]) -> None: + self.__authorization_code = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def operation_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the operation in merchant time zone in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__operation_timestamp + + @operation_timestamp.setter + def operation_timestamp(self, value: Optional[datetime]) -> None: + self.__operation_timestamp = value + + @property + def operation_type(self) -> Optional[str]: + """ + | The kind of operation + + Type: str + """ + return self.__operation_type + + @operation_type.setter + def operation_type(self, value: Optional[str]) -> None: + self.__operation_type = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the operation. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the operation can be retried later. + | PT0 means that the operation can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + def to_dictionary(self) -> dict: + dictionary = super(SubOperation, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.authorization_code is not None: + dictionary['authorizationCode'] = self.authorization_code + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.operation_timestamp is not None: + dictionary['operationTimestamp'] = DataObject.format_datetime(self.operation_timestamp) + if self.operation_type is not None: + dictionary['operationType'] = self.operation_type + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'SubOperation': + super(SubOperation, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'authorizationCode' in dictionary: + self.authorization_code = dictionary['authorizationCode'] + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'operationTimestamp' in dictionary: + self.operation_timestamp = DataObject.parse_datetime(dictionary['operationTimestamp']) + if 'operationType' in dictionary: + self.operation_type = dictionary['operationType'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.py b/worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.py new file mode 100644 index 0000000..d818b31 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class SubOperationForRefund(DataObject): + + __amount: Optional[AmountData] = None + __operation_id: Optional[str] = None + __operation_timestamp: Optional[datetime] = None + __operation_type: Optional[str] = None + __response_code: Optional[str] = None + __response_code_category: Optional[str] = None + __response_code_description: Optional[str] = None + __retry_after: Optional[str] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def operation_id(self) -> Optional[str]: + """ + | A globally unique identifier of the operation, generated by you. + | We advise you to submit a UUID or an identifier composed of an arbitrary string and a UUID/URL-safe Base64 UUID (RFC 4648 §5). + | It's used to detect duplicate requests or to reference an operation in technical reversals. + + Type: str + """ + return self.__operation_id + + @operation_id.setter + def operation_id(self, value: Optional[str]) -> None: + self.__operation_id = value + + @property + def operation_timestamp(self) -> Optional[datetime]: + """ + | Timestamp of the operation in merchant time zone in format yyyy-MM-ddTHH:mm:ssZ + + Type: datetime + """ + return self.__operation_timestamp + + @operation_timestamp.setter + def operation_timestamp(self, value: Optional[datetime]) -> None: + self.__operation_timestamp = value + + @property + def operation_type(self) -> Optional[str]: + """ + | The kind of operation + + Type: str + """ + return self.__operation_type + + @operation_type.setter + def operation_type(self, value: Optional[str]) -> None: + self.__operation_type = value + + @property + def response_code(self) -> Optional[str]: + """ + | Numeric response code, e.g. 0000, 0005 + + Type: str + """ + return self.__response_code + + @response_code.setter + def response_code(self, value: Optional[str]) -> None: + self.__response_code = value + + @property + def response_code_category(self) -> Optional[str]: + """ + | Category of response code. + + Type: str + """ + return self.__response_code_category + + @response_code_category.setter + def response_code_category(self, value: Optional[str]) -> None: + self.__response_code_category = value + + @property + def response_code_description(self) -> Optional[str]: + """ + | Description of the response code + + Type: str + """ + return self.__response_code_description + + @response_code_description.setter + def response_code_description(self, value: Optional[str]) -> None: + self.__response_code_description = value + + @property + def retry_after(self) -> Optional[str]: + """ + | The duration to wait after the initial submission before retrying the operation. + | Expressed using ISO 8601 duration format, ex: PT2H for 2 hours. + | This field is only present when the operation can be retried later. + | PT0 means that the operation can be retried immediately. + + Type: str + """ + return self.__retry_after + + @retry_after.setter + def retry_after(self, value: Optional[str]) -> None: + self.__retry_after = value + + def to_dictionary(self) -> dict: + dictionary = super(SubOperationForRefund, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.operation_id is not None: + dictionary['operationId'] = self.operation_id + if self.operation_timestamp is not None: + dictionary['operationTimestamp'] = DataObject.format_datetime(self.operation_timestamp) + if self.operation_type is not None: + dictionary['operationType'] = self.operation_type + if self.response_code is not None: + dictionary['responseCode'] = self.response_code + if self.response_code_category is not None: + dictionary['responseCodeCategory'] = self.response_code_category + if self.response_code_description is not None: + dictionary['responseCodeDescription'] = self.response_code_description + if self.retry_after is not None: + dictionary['retryAfter'] = self.retry_after + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'SubOperationForRefund': + super(SubOperationForRefund, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'operationId' in dictionary: + self.operation_id = dictionary['operationId'] + if 'operationTimestamp' in dictionary: + self.operation_timestamp = DataObject.parse_datetime(dictionary['operationTimestamp']) + if 'operationType' in dictionary: + self.operation_type = dictionary['operationType'] + if 'responseCode' in dictionary: + self.response_code = dictionary['responseCode'] + if 'responseCodeCategory' in dictionary: + self.response_code_category = dictionary['responseCodeCategory'] + if 'responseCodeDescription' in dictionary: + self.response_code_description = dictionary['responseCodeDescription'] + if 'retryAfter' in dictionary: + self.retry_after = dictionary['retryAfter'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.py b/worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.py new file mode 100644 index 0000000..6431017 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class SubsequentCardOnFileData(DataObject): + + __card_on_file_initiator: Optional[str] = None + __initial_scheme_transaction_id: Optional[str] = None + __transaction_type: Optional[str] = None + + @property + def card_on_file_initiator(self) -> Optional[str]: + """ + | Card on file initiator + + Type: str + """ + return self.__card_on_file_initiator + + @card_on_file_initiator.setter + def card_on_file_initiator(self, value: Optional[str]) -> None: + self.__card_on_file_initiator = value + + @property + def initial_scheme_transaction_id(self) -> Optional[str]: + """ + | Scheme transaction ID of initial transaction + + Type: str + """ + return self.__initial_scheme_transaction_id + + @initial_scheme_transaction_id.setter + def initial_scheme_transaction_id(self, value: Optional[str]) -> None: + self.__initial_scheme_transaction_id = value + + @property + def transaction_type(self) -> Optional[str]: + """ + | Transaction type + + Type: str + """ + return self.__transaction_type + + @transaction_type.setter + def transaction_type(self, value: Optional[str]) -> None: + self.__transaction_type = value + + def to_dictionary(self) -> dict: + dictionary = super(SubsequentCardOnFileData, self).to_dictionary() + if self.card_on_file_initiator is not None: + dictionary['cardOnFileInitiator'] = self.card_on_file_initiator + if self.initial_scheme_transaction_id is not None: + dictionary['initialSchemeTransactionId'] = self.initial_scheme_transaction_id + if self.transaction_type is not None: + dictionary['transactionType'] = self.transaction_type + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'SubsequentCardOnFileData': + super(SubsequentCardOnFileData, self).from_dictionary(dictionary) + if 'cardOnFileInitiator' in dictionary: + self.card_on_file_initiator = dictionary['cardOnFileInitiator'] + if 'initialSchemeTransactionId' in dictionary: + self.initial_scheme_transaction_id = dictionary['initialSchemeTransactionId'] + if 'transactionType' in dictionary: + self.transaction_type = dictionary['transactionType'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/three_d_secure.py b/worldline/acquiring/sdk/v1/domain/three_d_secure.py new file mode 100644 index 0000000..f5f7550 --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/three_d_secure.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class ThreeDSecure(DataObject): + + __authentication_value: Optional[str] = None + __directory_server_transaction_id: Optional[str] = None + __eci: Optional[str] = None + __three_d_secure_type: Optional[str] = None + __version: Optional[str] = None + + @property + def authentication_value(self) -> Optional[str]: + """ + | MasterCard AAV in original base64 encoding or Visa, DinersClub, UnionPay or JCB CAVV in either hexadecimal or base64 encoding + + Type: str + """ + return self.__authentication_value + + @authentication_value.setter + def authentication_value(self, value: Optional[str]) -> None: + self.__authentication_value = value + + @property + def directory_server_transaction_id(self) -> Optional[str]: + """ + | 3D Secure 2.x directory server transaction ID + + Type: str + """ + return self.__directory_server_transaction_id + + @directory_server_transaction_id.setter + def directory_server_transaction_id(self, value: Optional[str]) -> None: + self.__directory_server_transaction_id = value + + @property + def eci(self) -> Optional[str]: + """ + | Electronic Commerce Indicator + | Value returned by the 3D Secure process that indicates the level of authentication. + | Contains different values depending on the brand. + + Type: str + """ + return self.__eci + + @eci.setter + def eci(self, value: Optional[str]) -> None: + self.__eci = value + + @property + def three_d_secure_type(self) -> Optional[str]: + """ + | 3D Secure type used in the transaction + + Type: str + """ + return self.__three_d_secure_type + + @three_d_secure_type.setter + def three_d_secure_type(self, value: Optional[str]) -> None: + self.__three_d_secure_type = value + + @property + def version(self) -> Optional[str]: + """ + | 3D Secure version + + Type: str + """ + return self.__version + + @version.setter + def version(self, value: Optional[str]) -> None: + self.__version = value + + def to_dictionary(self) -> dict: + dictionary = super(ThreeDSecure, self).to_dictionary() + if self.authentication_value is not None: + dictionary['authenticationValue'] = self.authentication_value + if self.directory_server_transaction_id is not None: + dictionary['directoryServerTransactionId'] = self.directory_server_transaction_id + if self.eci is not None: + dictionary['eci'] = self.eci + if self.three_d_secure_type is not None: + dictionary['threeDSecureType'] = self.three_d_secure_type + if self.version is not None: + dictionary['version'] = self.version + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'ThreeDSecure': + super(ThreeDSecure, self).from_dictionary(dictionary) + if 'authenticationValue' in dictionary: + self.authentication_value = dictionary['authenticationValue'] + if 'directoryServerTransactionId' in dictionary: + self.directory_server_transaction_id = dictionary['directoryServerTransactionId'] + if 'eci' in dictionary: + self.eci = dictionary['eci'] + if 'threeDSecureType' in dictionary: + self.three_d_secure_type = dictionary['threeDSecureType'] + if 'version' in dictionary: + self.version = dictionary['version'] + return self diff --git a/worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.py b/worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.py new file mode 100644 index 0000000..955622f --- /dev/null +++ b/worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from datetime import datetime +from typing import Optional + +from .amount_data import AmountData + +from worldline.acquiring.sdk.domain.data_object import DataObject + + +class TransactionDataForDcc(DataObject): + + __amount: Optional[AmountData] = None + __transaction_timestamp: Optional[datetime] = None + __transaction_type: Optional[str] = None + + @property + def amount(self) -> Optional[AmountData]: + """ + | Amount for the operation. + + Type: :class:`worldline.acquiring.sdk.v1.domain.amount_data.AmountData` + """ + return self.__amount + + @amount.setter + def amount(self, value: Optional[AmountData]) -> None: + self.__amount = value + + @property + def transaction_timestamp(self) -> Optional[datetime]: + """ + | The date and time of the transaction + + Type: datetime + """ + return self.__transaction_timestamp + + @transaction_timestamp.setter + def transaction_timestamp(self, value: Optional[datetime]) -> None: + self.__transaction_timestamp = value + + @property + def transaction_type(self) -> Optional[str]: + """ + | The transaction type + + Type: str + """ + return self.__transaction_type + + @transaction_type.setter + def transaction_type(self, value: Optional[str]) -> None: + self.__transaction_type = value + + def to_dictionary(self) -> dict: + dictionary = super(TransactionDataForDcc, self).to_dictionary() + if self.amount is not None: + dictionary['amount'] = self.amount.to_dictionary() + if self.transaction_timestamp is not None: + dictionary['transactionTimestamp'] = DataObject.format_datetime(self.transaction_timestamp) + if self.transaction_type is not None: + dictionary['transactionType'] = self.transaction_type + return dictionary + + def from_dictionary(self, dictionary: dict) -> 'TransactionDataForDcc': + super(TransactionDataForDcc, self).from_dictionary(dictionary) + if 'amount' in dictionary: + if not isinstance(dictionary['amount'], dict): + raise TypeError('value \'{}\' is not a dictionary'.format(dictionary['amount'])) + value = AmountData() + self.amount = value.from_dictionary(dictionary['amount']) + if 'transactionTimestamp' in dictionary: + self.transaction_timestamp = DataObject.parse_datetime(dictionary['transactionTimestamp']) + if 'transactionType' in dictionary: + self.transaction_type = dictionary['transactionType'] + return self diff --git a/worldline/acquiring/sdk/v1/exception_factory.py b/worldline/acquiring/sdk/v1/exception_factory.py new file mode 100644 index 0000000..2d0a518 --- /dev/null +++ b/worldline/acquiring/sdk/v1/exception_factory.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Any, Optional + +from .api_exception import ApiException +from .authorization_exception import AuthorizationException +from .platform_exception import PlatformException +from .reference_exception import ReferenceException +from .validation_exception import ValidationException + +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse + + +def create_exception(status_code: int, body: str, error_object: Any, context: Optional[CallContext]) -> Exception: + """Return a raisable API exception based on the error object given""" + def create_exception_from_response_fields(type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str]) -> Exception: + # get error based on status code, defaulting to ApiException + return ERROR_MAP.get(status_code, ApiException)(status_code, body, type, title, status, detail, instance) + + if not isinstance(error_object, ApiPaymentErrorResponse): + raise ValueError("Unsupported error object encountered: {}".format(error_object.__class__.__name__)) + + return create_exception_from_response_fields(error_object.type, error_object.title, error_object.status, error_object.detail, error_object.instance) + + +ERROR_MAP = { + 400: ValidationException, + 403: AuthorizationException, + 404: ReferenceException, + 409: ReferenceException, + 410: ReferenceException, + 500: PlatformException, + 502: PlatformException, + 503: PlatformException, +} diff --git a/worldline/acquiring/sdk/v1/ping/__init__.py b/worldline/acquiring/sdk/v1/ping/__init__.py new file mode 100644 index 0000000..ca1222f --- /dev/null +++ b/worldline/acquiring/sdk/v1/ping/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# diff --git a/worldline/acquiring/sdk/v1/ping/ping_client.py b/worldline/acquiring/sdk/v1/ping/ping_client.py new file mode 100644 index 0000000..e1b2229 --- /dev/null +++ b/worldline/acquiring/sdk/v1/ping/ping_client.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.call_context import CallContext +from worldline.acquiring.sdk.communication.response_exception import ResponseException +from worldline.acquiring.sdk.v1.domain.api_payment_error_response import ApiPaymentErrorResponse +from worldline.acquiring.sdk.v1.exception_factory import create_exception + + +class PingClient(ApiResource): + """ + Ping client. Thread-safe. + """ + + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(PingClient, self).__init__(parent=parent, path_context=path_context) + + def ping(self, context: Optional[CallContext] = None) -> None: + """ + Resource /services/v1/ping - Check API connection + + See also https://docs.acquiring.worldline-solutions.com/api-reference#tag/Ping/operation/ping + + :param context: :class:`worldline.acquiring.sdk.call_context.CallContext` + :return: None + :raise ValidationException: if the request was not correct and couldn't be processed (HTTP status code 400) + :raise AuthorizationException: if the request was not allowed (HTTP status code 403) + :raise ReferenceException: if an object was attempted to be referenced that doesn't exist or has been removed, + or there was a conflict (HTTP status code 404, 409 or 410) + :raise PlatformException: if something went wrong at the Worldline Acquiring platform, + the Worldline Acquiring platform was unable to process a message from a downstream partner/acquirer, + or the service that you're trying to reach is temporary unavailable (HTTP status code 500, 502 or 503) + :raise ApiException: if the Worldline Acquiring platform returned any other error + """ + uri = self._instantiate_uri("/services/v1/ping", None) + try: + return self._communicator.get( + uri, + None, + None, + None, + context) + + except ResponseException as e: + error_type = ApiPaymentErrorResponse + error_object = self._communicator.marshaller.unmarshal(e.body, error_type) + raise create_exception(e.status_code, e.body, error_object, context) diff --git a/worldline/acquiring/sdk/v1/platform_exception.py b/worldline/acquiring/sdk/v1/platform_exception.py new file mode 100644 index 0000000..ecd9ebd --- /dev/null +++ b/worldline/acquiring/sdk/v1/platform_exception.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_exception import ApiException + + +class PlatformException(ApiException): + """ + Represents an error response from the Worldline Acquiring platform when something went wrong at the Worldline Acquiring platform or further downstream. + """ + + def __init__(self, status_code: int, response_body: str, type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str], + message: str = "The Worldline Acquiring platform returned an error response"): + super(PlatformException, self).__init__(status_code, response_body, type, title, status, detail, instance, message) diff --git a/worldline/acquiring/sdk/v1/reference_exception.py b/worldline/acquiring/sdk/v1/reference_exception.py new file mode 100644 index 0000000..f9e92d9 --- /dev/null +++ b/worldline/acquiring/sdk/v1/reference_exception.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_exception import ApiException + + +class ReferenceException(ApiException): + """ + Represents an error response from the Worldline Acquiring platform when a non-existing or removed object is trying to be accessed. + """ + + def __init__(self, status_code: int, response_body: str, type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str], + message: str = "The Worldline Acquiring platform returned a reference error response"): + super(ReferenceException, self).__init__(status_code, response_body, type, title, status, detail, instance, message) diff --git a/worldline/acquiring/sdk/v1/v1_client.py b/worldline/acquiring/sdk/v1/v1_client.py new file mode 100644 index 0000000..91f1154 --- /dev/null +++ b/worldline/acquiring/sdk/v1/v1_client.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Mapping, Optional + +from worldline.acquiring.sdk.api_resource import ApiResource +from worldline.acquiring.sdk.v1.acquirer.acquirer_client import AcquirerClient +from worldline.acquiring.sdk.v1.ping.ping_client import PingClient + + +class V1Client(ApiResource): + """ + V1 client. + + Thread-safe. + """ + def __init__(self, parent: ApiResource, path_context: Optional[Mapping[str, str]]): + """ + :param parent: :class:`worldline.acquiring.sdk.api_resource.ApiResource` + :param path_context: Mapping[str, str] + """ + super(V1Client, self).__init__(parent=parent, path_context=path_context) + + def acquirer(self, acquirer_id: str) -> AcquirerClient: + """ + Resource /processing/v1/{acquirerId} + + :param acquirer_id: str + :return: :class:`worldline.acquiring.sdk.v1.acquirer.acquirer_client.AcquirerClient` + """ + sub_context = { + "acquirerId": acquirer_id, + } + return AcquirerClient(self, sub_context) + + def ping(self) -> PingClient: + """ + Resource /services/v1/ping + + :return: :class:`worldline.acquiring.sdk.v1.ping.ping_client.PingClient` + """ + return PingClient(self, None) diff --git a/worldline/acquiring/sdk/v1/validation_exception.py b/worldline/acquiring/sdk/v1/validation_exception.py new file mode 100644 index 0000000..114b380 --- /dev/null +++ b/worldline/acquiring/sdk/v1/validation_exception.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# This file was automatically generated. +# +from typing import Optional + +from .api_exception import ApiException + + +class ValidationException(ApiException): + """ + Represents an error response from the Worldline Acquiring platform when validation of requests failed. + """ + + def __init__(self, status_code: int, response_body: str, type: Optional[str], title: Optional[str], status: Optional[int], detail: Optional[str], instance: Optional[str], + message: str = "The Worldline Acquiring platform returned an incorrect request error response"): + super(ValidationException, self).__init__(status_code, response_body, type, title, status, detail, instance, message)