├── .built ├── .bundled ├── tests ├── __init__.py ├── webpay │ ├── __init__.py │ ├── oneclick │ │ ├── __init__.py │ │ ├── test_mall_bin_info.py │ │ ├── test_mall_inscription.py │ │ └── test_mall_transaction.py │ ├── plus │ │ ├── __init__.py │ │ ├── test_transaction.py │ │ └── test_mall_transaction.py │ ├── transaccion_completa │ │ ├── __init__.py │ │ ├── test_transaction.py │ │ └── test_mall_transaction.py │ └── test_utils.py ├── patpass_comercio │ ├── __init__.py │ └── test_inscription.py └── mocks │ ├── transaccion_completa_responses_api_mocks.py │ ├── patpass_responses_api_mocks.py │ └── responses_api_mocks.py ├── transbank ├── common │ ├── __init__.py │ ├── schema.py │ ├── model │ │ └── __init__.py │ ├── integration_api_keys.py │ ├── headers_builder.py │ ├── api_constants.py │ ├── webpay_transaction.py │ ├── integration_type.py │ ├── validation_util.py │ ├── options.py │ ├── integration_commerce_codes.py │ └── request_service.py ├── error │ ├── __init__.py │ ├── mall_bin_info_query_error.py │ ├── transaction_capture_error.py │ ├── transaction_commit_error.py │ ├── transaction_create_error.py │ ├── inscription_status_error.py │ ├── transaction_refund_error.py │ ├── transaction_status_error.py │ ├── transaction_authorize_error.py │ ├── inscription_delete_error.py │ ├── inscription_finish_error.py │ ├── inscription_start_error.py │ ├── transaction_installments_error.py │ ├── transbank_error.py │ └── invalid_amount_error.py ├── webpay │ ├── __init__.py │ ├── oneclick │ │ ├── __init__.py │ │ ├── schema.py │ │ ├── mall_bin_info.py │ │ ├── mall_inscription.py │ │ ├── request │ │ │ └── __init__.py │ │ └── mall_transaction.py │ ├── webpay_plus │ │ ├── __init__.py │ │ ├── schema.py │ │ ├── mall_schema.py │ │ ├── request │ │ │ └── __init__.py │ │ ├── transaction.py │ │ └── mall_transaction.py │ └── transaccion_completa │ │ ├── __init__.py │ │ ├── schema.py │ │ ├── mall_schema.py │ │ ├── request │ │ └── __init__.py │ │ ├── mall_request │ │ └── __init__.py │ │ ├── transaction.py │ │ └── mall_transaction.py ├── validators │ ├── __init__.py │ └── amount_validator.py ├── patpass_comercio │ ├── __init__.py │ ├── schema.py │ ├── request │ │ └── __init__.py │ └── inscription.py ├── __init__.py └── __version__.py ├── MANIFEST.in ├── Dockerfile ├── docker-unit-test ├── run.sh ├── DockerfileTemplate ├── build.sh └── README.md ├── setup.cfg ├── Makefile ├── .editorconfig ├── docker-compose.yml ├── Pipfile ├── sonar-project.properties ├── tox.ini ├── .github ├── workflows │ ├── sonar.yml │ └── publish.yml └── ISSUE_TEMPLATE │ └── reporte-de-error.md ├── LICENSE ├── .gitignore ├── setup.py ├── README.md ├── CHANGELOG.md └── Pipfile.lock /.built: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bundled: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/webpay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/error/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/webpay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/webpay/oneclick/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/webpay/plus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/validators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/patpass_comercio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/patpass_comercio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/webpay/transaccion_completa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/__init__.py: -------------------------------------------------------------------------------- 1 | from transbank import * 2 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transbank/__version__.py: -------------------------------------------------------------------------------- 1 | VERSION = (6, 1, 0) 2 | 3 | __version__ = '.'.join(map(str, VERSION)) 4 | -------------------------------------------------------------------------------- /transbank/common/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class CardDetailSchema(Schema): 5 | card_number = fields.Str() 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.0a5-slim 2 | RUN apt-get update && apt-get install -y python3-pip 3 | RUN pip install pipenv 4 | RUN mkdir -p /sdk 5 | WORKDIR /sdk 6 | COPY . /sdk 7 | -------------------------------------------------------------------------------- /docker-unit-test/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Run container docker for name: tbk-sdk-python-unittest" 3 | 4 | #run the docker 5 | docker run --rm tbk-sdk-python-unittest 6 | -------------------------------------------------------------------------------- /tests/mocks/transaccion_completa_responses_api_mocks.py: -------------------------------------------------------------------------------- 1 | responses = { 2 | 'create_response': { 3 | 'token': '6172f122a4f7df9abc608400a378fa9c029f1f858380bc3ab52557f811986d98' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=1 3 | detailed-errors=1 4 | with-coverage=1 5 | cover-package=transbank 6 | 7 | [coverage:report] 8 | exclude_lines = 9 | raise NotImplementedError 10 | pragma: no cover 11 | -------------------------------------------------------------------------------- /transbank/common/model/__init__.py: -------------------------------------------------------------------------------- 1 | class CardDetail(object): 2 | def __init__(self, card_number: str): 3 | self.card_number = card_number 4 | 5 | def __repr__(self) -> str: 6 | return "CardDetail(card_number: {})".format(self.card_number) 7 | -------------------------------------------------------------------------------- /transbank/common/integration_api_keys.py: -------------------------------------------------------------------------------- 1 | # Contains the Webpay, Oneclick and Patpass Comercio constants for testing. 2 | class IntegrationApiKeys(object): 3 | WEBPAY = "579B532A7440BB0C9079DED94D31EA1615BACEB56610332264630D42D0A36B1C" 4 | PATPASS_COMERCIO = "cxxXQgGD9vrVe4M41FIt" 5 | -------------------------------------------------------------------------------- /docker-unit-test/DockerfileTemplate: -------------------------------------------------------------------------------- 1 | FROM python:version-number 2 | RUN apt-get update && apt-get install -y python3-pip 3 | RUN pip install pipenv 4 | RUN mkdir -p /sdk 5 | WORKDIR /sdk 6 | COPY . /sdk 7 | RUN pipenv install --dev --skip-lock 8 | RUN pip --version 9 | ENTRYPOINT ["sh", "-c","pipenv run tests"] 10 | -------------------------------------------------------------------------------- /transbank/error/mall_bin_info_query_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | class MallBinInfoQueryError(TransbankError): 4 | def __init__(self, message="Mall bin info query could not be performed. Please verify given parameters", code=0): 5 | super().__init__(message, code) 6 | -------------------------------------------------------------------------------- /transbank/error/transaction_capture_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionCaptureError(TransbankError): 5 | def __init__(self, message="Transaction could not be Captured. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transaction_commit_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionCommitError(TransbankError): 5 | def __init__(self, message = "Transaction could not be committed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transaction_create_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionCreateError(TransbankError): 5 | def __init__(self, message = "Transaction could not be created. Please verify given parameters", code = 0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/inscription_status_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class InscriptionStatusError(TransbankError): 5 | def __init__(self, message="Inscription status could not be performed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transaction_refund_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionRefundError(TransbankError): 5 | def __init__(self, message="Transaction refund could not be performed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transaction_status_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionStatusError(TransbankError): 5 | def __init__(self, message="Transaction status could not be performed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transaction_authorize_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionAuthorizeError(TransbankError): 5 | def __init__(self, message="Transaction authorize could not be performed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/inscription_delete_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class InscriptionDeleteError(TransbankError): 5 | def __init__(self, message="Inscription delete could not be performed. Please verify given parameters", 6 | code=0): 7 | super().__init__(message, code) 8 | -------------------------------------------------------------------------------- /transbank/error/inscription_finish_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class InscriptionFinishError(TransbankError): 5 | def __init__(self, message="Inscription finish could not be performed. Please verify given parameters", 6 | code=0): 7 | super().__init__(message, code) 8 | -------------------------------------------------------------------------------- /transbank/error/inscription_start_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class InscriptionStartError(TransbankError): 5 | def __init__(self, message="Inscription start could not be performed. Please verify given parameters", 6 | code=0): 7 | super().__init__(message, code) 8 | -------------------------------------------------------------------------------- /transbank/error/transaction_installments_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class TransactionInstallmentsError(TransbankError): 5 | def __init__(self, message="Transaction installments could not be performed. Please verify given parameters", code=0): 6 | super().__init__(message, code) 7 | -------------------------------------------------------------------------------- /transbank/error/transbank_error.py: -------------------------------------------------------------------------------- 1 | class TransbankError(Exception): 2 | def __init__(self, message = "An error has happened, verify given parameters and try again.", code = 0): 3 | super() 4 | self.message = message 5 | self.code = code 6 | 7 | def __repr__(self): 8 | return "message: {}, code: {}".format(self.message, self.code) 9 | -------------------------------------------------------------------------------- /docker-unit-test/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=3.7.4-stretch 4 | if [ "$1" != "" ];then 5 | VERSION=$1 6 | fi 7 | 8 | cp ../Dockerfile ../Dockerfile2 9 | 10 | sed -e "s/version-number/$VERSION/g" DockerfileTemplate > ../Dockerfile 11 | docker build -t "tbk-sdk-python-unittest" .. 12 | 13 | cp ../Dockerfile2 ../Dockerfile 14 | rm -rf ../Dockerfile2 15 | -------------------------------------------------------------------------------- /tests/webpay/test_utils.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | from transbank.common.api_constants import ApiConstants 4 | 5 | 6 | def get_invalid_length_param() -> str: 7 | valid_string = string.ascii_letters + string.digits + "-._~" 8 | invalid_length_param = ''.join(secrets.choice(valid_string) for _ in range(ApiConstants.RETURN_URL_LENGTH + 1)) 9 | return invalid_length_param 10 | -------------------------------------------------------------------------------- /transbank/common/headers_builder.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import Options 2 | 3 | 4 | class HeadersBuilder(object): 5 | @staticmethod 6 | def build(options: Options): 7 | return { 8 | "Content-Type": "application/json", 9 | options.header_commerce_code_name(): options.commerce_code, 10 | options.header_api_key_name(): options.api_key 11 | } 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | all: build run 4 | 5 | run: build 6 | docker-compose run --rm web 7 | 8 | build: .built .bundled 9 | 10 | .built: Dockerfile 11 | docker-compose build 12 | touch .built 13 | 14 | .bundled: Pipfile 15 | docker-compose run web pipenv install 16 | touch .bundled 17 | 18 | logs: 19 | docker-compose logs 20 | 21 | clean: 22 | docker-compose rm 23 | rm .built 24 | rm .bundled 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | 23 | [*.{diff,patch}] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: &web 4 | tty: true 5 | stdin_open: true 6 | build: 7 | context: . 8 | dockerfile: docker-unit-test/Dockerfile 9 | command: bash 10 | volumes: 11 | - .:/sdk 12 | volumes_from: 13 | - python_cache 14 | ports: 15 | - "8000:8000" 16 | 17 | python_cache: 18 | image: python:3.7.4-stretch 19 | volumes: 20 | - /usr/local/lib/python3.7/site-packages 21 | -------------------------------------------------------------------------------- /tests/mocks/patpass_responses_api_mocks.py: -------------------------------------------------------------------------------- 1 | responses = { 2 | 'inscription_response': { 3 | 'token': '6172f122a4f7df9abc608400a378fa9c029f1f858380bc3ab52557f811986d98', 4 | 'url': 'https://pagoautomaticocontarjetasint.transbank.cl/nuevo-ic-rest/tokenComercioLogin' 5 | }, 6 | 'status_response': { 7 | 'authorized': True, 8 | 'voucherUrl': 'https://pagoautomaticocontarjetasint.transbank.cl/nuevo-ic-rest/tokenVoucherLogin' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /transbank/error/invalid_amount_error.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | 4 | class InvalidAmountError(TransbankError): 5 | DEFAULT_MESSAGE = 'Invalid amount given.' 6 | NOT_NUMERIC_MESSAGE = 'Given amount is not numeric.' 7 | HAS_DECIMALS_MESSAGE = 'Given amount has decimals. Webpay only accepts integer amounts. Please remove decimal places.' 8 | 9 | def __init__(self, message=DEFAULT_MESSAGE, code=0): 10 | super().__init__(message, code) 11 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [requires] 7 | python_version = "3.12" 8 | 9 | [dev-packages] 10 | ipython = ">=9.3.0" 11 | docutils = "*" 12 | coverage = "*" 13 | pylint = "*" 14 | requests-mock = "<=1.12.1" 15 | pytest-cov = "*" 16 | pytest = "*" 17 | 18 | [packages] 19 | marshmallow = ">=4.0.0" 20 | requests = ">=2.32.4" 21 | mock = "*" 22 | setuptools = ">=80.9.0" 23 | 24 | [scripts] 25 | tests = "pytest" 26 | citests = "pytest --cov-report=xml" 27 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=TransbankDevelopers_transbank-sdk-python 2 | sonar.organization=transbankdevelopers 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=Transbank Python SDK 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | 14 | sonar.python.coverage.reportPaths=coverage.xml 15 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | from transbank.common.schema import CardDetailSchema 4 | 5 | class TransactionCreateRequestSchema(Schema): 6 | buy_order = fields.Str() 7 | session_id = fields.Str() 8 | amount = fields.Str() 9 | return_url = fields.Str() 10 | 11 | class TransactionRefundRequestSchema(Schema): 12 | amount = fields.Str() 13 | 14 | class TransactionCaptureRequestSchema(Schema): 15 | buy_order = fields.Str() 16 | capture_amount = fields.Str() 17 | authorization_code = fields.Str() 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = test, cov 3 | skipsdist = true 4 | 5 | [testenv:test] 6 | description = Ejecuta los tests unitarios sin coverage 7 | deps = 8 | marshmallow 9 | requests 10 | commands = 11 | python -m unittest discover -s tests 12 | 13 | [testenv:cov] 14 | description = Ejecuta los tests unitarios con coverage 15 | deps = 16 | marshmallow 17 | requests 18 | coverage 19 | commands = 20 | coverage run -m unittest discover -s tests 21 | coverage xml 22 | 23 | [coverage:run] 24 | relative_files = true 25 | source = transbank 26 | branch = true 27 | -------------------------------------------------------------------------------- /transbank/common/api_constants.py: -------------------------------------------------------------------------------- 1 | class ApiConstants(object): 2 | WEBPAY_ENDPOINT = "/rswebpaytransaction/api/webpay/v1.2" 3 | ONECLICK_ENDPOINT = "/rswebpaytransaction/api/oneclick/v1.2" 4 | PATPASS_ENDPOINT = "/restpatpass/v1/services" 5 | 6 | BUY_ORDER_LENGTH = 26 7 | SESSION_ID_LENGTH = 61 8 | RETURN_URL_LENGTH = 255 9 | AUTHORIZATION_CODE_LENGTH = 6 10 | CARD_EXPIRATION_DATE_LENGTH = 5 11 | CARD_NUMBER_LENGTH = 19 12 | TBK_USER_LENGTH = 40 13 | USER_NAME_LENGTH = 40 14 | COMMERCE_CODE_LENGTH = 12 15 | TOKEN_LENGTH = 64 16 | EMAIL_LENGTH = 100 17 | HTTP_STATUS_DELETE_OK = 204 18 | -------------------------------------------------------------------------------- /transbank/common/webpay_transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.integration_type import IntegrationType 3 | 4 | class WebpayTransaction(object): 5 | def __init__(self, options: WebpayOptions): 6 | self.options = options 7 | 8 | @classmethod 9 | def build_for_integration(cls, commerce_code, api_key): 10 | options = WebpayOptions(commerce_code, api_key, IntegrationType.TEST) 11 | return cls(options) 12 | 13 | @classmethod 14 | def build_for_production(cls, commerce_code, api_key): 15 | options = WebpayOptions(commerce_code, api_key, IntegrationType.LIVE) 16 | return cls(options) 17 | -------------------------------------------------------------------------------- /transbank/validators/amount_validator.py: -------------------------------------------------------------------------------- 1 | from transbank.error.invalid_amount_error import InvalidAmountError 2 | 3 | 4 | class AmountValidator: 5 | 6 | @staticmethod 7 | def validate(amount, nullable=False): 8 | if nullable and amount is None: 9 | return 10 | try: 11 | float(amount) 12 | except ValueError: 13 | raise InvalidAmountError(InvalidAmountError.NOT_NUMERIC_MESSAGE) 14 | amount_str = str(amount) 15 | if amount_str.startswith("-"): # ignore sign 16 | amount_str = amount_str[1:] 17 | if not str.isdigit(amount_str): 18 | raise InvalidAmountError(InvalidAmountError.HAS_DECIMALS_MESSAGE) 19 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | from transbank.common.schema import CardDetailSchema 3 | 4 | class TransactionCreateRequestSchema(Schema): 5 | buy_order = fields.Str() 6 | session_id = fields.Str() 7 | amount = fields.Str() 8 | card_number = fields.Str() 9 | cvv = fields.Str() 10 | card_expiration_date = fields.Str() 11 | 12 | class TransactionCommitRequestSchema(Schema): 13 | id_query_installments = fields.Str() 14 | deferred_periods_index = fields.Str() 15 | grace_period = fields.Str() 16 | 17 | class TransactionInstallmentsRequestSchema(Schema): 18 | installments_number = fields.Float() 19 | 20 | class TransactionRefundRequestSchema(Schema): 21 | amount = fields.Str() 22 | 23 | class TransactionCaptureRequestSchema(Schema): 24 | buy_order = fields.Str() 25 | authorization_code = fields.Str() 26 | capture_amount = fields.Str() 27 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar Scan 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | sonarqube: 11 | name: SonarQube 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | - name: Install tox 20 | run: | 21 | pip install tox 22 | - name: Run Tox 23 | run: | 24 | tox -e cov 25 | - name: SonarQube Scan 26 | uses: SonarSource/sonarqube-scan-action@v5 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 30 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/mall_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | class TransactionCreateRequestSchema(Schema): 4 | buy_order = fields.Str() 5 | session_id = fields.Str() 6 | card_number = fields.Str() 7 | card_expiration_date = fields.Str() 8 | details = fields.List(fields.Raw()) 9 | cvv = fields.Str() 10 | 11 | class TransactionCommitRequestSchema(Schema): 12 | details = fields.List(fields.Raw()) 13 | 14 | class TransactionInstallmentsRequestSchema(Schema): 15 | installments_number = fields.Float() 16 | buy_order = fields.Str() 17 | commerce_code = fields.Str() 18 | 19 | class TransactionRefundRequestSchema(Schema): 20 | amount = fields.Str() 21 | commerce_code = fields.Str() 22 | buy_order = fields.Str() 23 | 24 | class TransactionCaptureRequestSchema(Schema): 25 | commerce_code = fields.Str() 26 | buy_order = fields.Str() 27 | authorization_code = fields.Str() 28 | capture_amount = fields.Str() 29 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/mall_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | class MallTransactionRefundRequestSchema(Schema): 4 | amount = fields.Str() 5 | commerce_code = fields.Str() 6 | buy_order = fields.Str() 7 | 8 | class MallDetailsSchema(Schema): 9 | amount = fields.Str() 10 | commerce_code = fields.Str() 11 | buy_order = fields.Str() 12 | status = fields.Str() 13 | authorization_code = fields.Str() 14 | payment_type_code = fields.Str() 15 | response_code = fields.Int() 16 | installments_number = fields.Int() 17 | 18 | class MallTransactionCreateRequestSchema(Schema): 19 | buy_order = fields.Str() 20 | session_id = fields.Str() 21 | return_url = fields.Str() 22 | details = fields.Nested(MallDetailsSchema, many=True) 23 | 24 | class MallTransactionCaptureRequestSchema(Schema): 25 | commerce_code = fields.Str() 26 | buy_order = fields.Str() 27 | authorization_code = fields.Str() 28 | capture_amount = fields.Str() 29 | -------------------------------------------------------------------------------- /transbank/patpass_comercio/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class InscriptionStartRequestSchema(Schema): 5 | url = fields.Str() 6 | nombre = fields.Str() 7 | pApellido = fields.Str() 8 | sApellido = fields.Str() 9 | rut = fields.Str() 10 | serviceId = fields.Str() 11 | finalUrl = fields.Str() 12 | commerceCode = fields.Str() 13 | montoMaximo = fields.Float() 14 | telefonoFijo = fields.Str() 15 | telefonoCelular = fields.Str() 16 | nombrePatPass = fields.Str() 17 | correoPersona = fields.Str() 18 | correoComercio = fields.Str() 19 | direccion = fields.Str() 20 | ciudad = fields.Str() 21 | 22 | 23 | class InscriptionStartResponseSchema(Schema): 24 | token = fields.Str() 25 | url = fields.Str() 26 | 27 | 28 | class InscriptionStatusRequestSchema(Schema): 29 | token = fields.Str() 30 | 31 | 32 | class InscriptionStatusResponseSchema(Schema): 33 | authorized = fields.Bool() 34 | voucherUrl = fields.Str() 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/reporte-de-error.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reporte de Error 3 | about: Crea un reporte para ayudarnos a mejorar 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe el bug** 11 | 12 | Una descripción concisa sobre el bug. 13 | 14 | **Para reproducir** 15 | 16 | 1. Configura '...' 17 | 2. Crea un objeto '...' 18 | 3. Ejecuta el método '...' 19 | 4. Se ve el error en '...' 20 | 21 | **Comportamiento observado** 22 | 23 | Describe de forma concisa lo que observaste siguiendo los pasos para reproducir el error. 24 | 25 | **Comportamiento esperado** 26 | 27 | Una explicación concisa y clara de qué es lo que esperas que ocurra. 28 | 29 | **Capturas de pantalla** 30 | 31 | Si aplica, agrega aquí capturas de pantalla que ayuden a explicar tu problema. 32 | 33 | **Versiones (por favor agrega aquí la siguiente información):** 34 | - SDK: [e.g. 1.1.0] 35 | - Python: [e.g. 3.4] 36 | 37 | **Contexto adicional** 38 | 39 | Agrega cualquier otro información sobre el problema aquí. 40 | -------------------------------------------------------------------------------- /transbank/common/integration_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | @unique 5 | class IntegrationType(Enum): 6 | LIVE = 1 7 | TEST = 2 8 | MOCK = 3 9 | 10 | def webpay_host(integration_type: IntegrationType) -> str: 11 | if integration_type is IntegrationType.LIVE: 12 | return "https://webpay3g.transbank.cl" 13 | 14 | if integration_type is IntegrationType.TEST: 15 | return "https://webpay3gint.transbank.cl" 16 | 17 | if integration_type is IntegrationType.MOCK: 18 | return None 19 | 20 | return "https://webpay3gint.transbank.cl" 21 | 22 | 23 | def patpass_comercio_host(integration_type: IntegrationType) -> str: 24 | if integration_type is IntegrationType.LIVE: 25 | return "https://www.pagoautomaticocontarjetas.cl" 26 | 27 | if integration_type is IntegrationType.TEST: 28 | return "https://pagoautomaticocontarjetasint.transbank.cl" 29 | 30 | if integration_type is IntegrationType.MOCK: 31 | return None 32 | 33 | return "https://pagoautomaticocontarjetasint.transbank.cl" 34 | -------------------------------------------------------------------------------- /transbank/common/validation_util.py: -------------------------------------------------------------------------------- 1 | from transbank.error.transbank_error import TransbankError 2 | 3 | class ValidationUtil(object): 4 | 5 | @staticmethod 6 | def has_text(value: str, value_name: str): 7 | if not value or not value.strip(): 8 | raise TransbankError("'{}' can't be null or white space".format(value_name)) 9 | 10 | @staticmethod 11 | def has_text_with_max_length(value: str, value_max_length: int, value_name: str): 12 | ValidationUtil.has_text(value, value_name) 13 | if len(value) > value_max_length: 14 | raise TransbankError("'{}' is too long, the maximum length is {}".format(value_name, value_max_length)) 15 | 16 | @staticmethod 17 | def has_text_trim_with_max_length(value: str, value_max_length: int, value_name: str): 18 | ValidationUtil.has_text_with_max_length(value, value_max_length, value_name) 19 | if len(value) > len(value.strip()): 20 | raise TransbankError("'{}' has spaces at the beginning or the end".format(value_name)) 21 | 22 | @staticmethod 23 | def has_elements(value: any, value_name: str): 24 | if value == None or len(value) == 0: 25 | raise TransbankError("list '{}'" + " can't be null or empty".format(value_name)) 26 | -------------------------------------------------------------------------------- /docker-unit-test/README.md: -------------------------------------------------------------------------------- 1 | # SDK Python 2 | 3 | 4 | ### Requerimientos 5 | 6 | **MacOS:** 7 | 8 | Instalar [Docker](https://docs.docker.com/docker-for-mac/install/), [Docker-compose](https://docs.docker.com/compose/install/#install-compose) 9 | 10 | **Windows:** 11 | 12 | Instalar [Docker](https://docs.docker.com/docker-for-windows/install/), [Docker-compose](https://docs.docker.com/compose/install/#install-compose) 13 | 14 | **Linux:** 15 | 16 | Instalar [Docker](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/) y [Docker-compose](https://docs.docker.com/compose/install/#install-compose). 17 | 18 | ### Como usar 19 | 20 | Esta imagin de docker sirve para correr y comprobar la compatibilidad del sdk con distintas versiones de python 21 | 22 | **NOTA:** La primera vez que se ejecuta ./build.sh demorará en instalar todo, esperar al menos unos 5 minutos. 23 | 24 | ### Construir el contenedor desde cero 25 | 26 | Para construir la imagen se debe ejecutar el archivo build.sh y se puede pasar opcionalmente 27 | la version de python, sino se pasa la version como parametro usara la version 3.7.4-stretch por defecto 28 | 29 | ``` 30 | 31 | ./build.sh 3.5.6 32 | ``` 33 | 34 | ### Iniciar el contenedor construido anteriormente 35 | 36 | ``` 37 | ./run.sh 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | from transbank.common.schema import CardDetailSchema 4 | 5 | class MallInscriptionStartRequestSchema(Schema): 6 | username = fields.Str() 7 | email = fields.Str() 8 | response_url = fields.Str() 9 | 10 | class MallInscriptionDeleteRequestSchema(Schema): 11 | username = fields.Str() 12 | tbk_user = fields.Str() 13 | 14 | 15 | class MallDetailsSchema(Schema): 16 | commerce_code = fields.Str() 17 | buy_order = fields.Str() 18 | installments_number = fields.Int() 19 | amount = fields.Str() 20 | 21 | 22 | class MallTransactionAuthorizeRequestSchema(Schema): 23 | username = fields.Str() 24 | tbk_user = fields.Str() 25 | buy_order = fields.Str() 26 | details = fields.Nested(MallDetailsSchema, many=True) 27 | 28 | class MallTransactionCaptureRequestSchema(Schema): 29 | commerce_code = fields.Str() 30 | buy_order = fields.Str() 31 | authorization_code = fields.Str() 32 | capture_amount = fields.Str() 33 | 34 | class MallTransactionRefundRequestSchema(Schema): 35 | commerce_code = fields.Str() 36 | detail_buy_order = fields.Str() 37 | amount = fields.Str() 38 | 39 | class MallBinInfoQueryRequestSchema(Schema): 40 | tbk_user = fields.Str() 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.12" 23 | 24 | - name: Install pipenv 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pipenv 28 | 29 | - name: Install dependencies 30 | run: pipenv install --dev 31 | 32 | - name: Install build tools 33 | run: pipenv install build twine wheel 34 | 35 | - name: Build package 36 | run: pipenv run python -m build --sdist --wheel 37 | 38 | - name: Verify distribution files 39 | run: pipenv run twine check dist/* 40 | 41 | - name: Publish to PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | with: 44 | repository-url: https://upload.pypi.org/legacy/ 45 | -------------------------------------------------------------------------------- /transbank/patpass_comercio/request/__init__.py: -------------------------------------------------------------------------------- 1 | class InscriptionStartRequest(object): 2 | def __init__(self, 3 | url: str, 4 | name: str, 5 | first_last_name: str, 6 | second_last_name: str, 7 | rut: str, 8 | service_id: str, 9 | final_url: str, 10 | commerce_code: str, 11 | max_amount: float, 12 | phone_number: str, 13 | mobile_number: str, 14 | patpass_name: str, 15 | person_email: str, 16 | commerce_email: str, 17 | address: str, 18 | city: str): 19 | self.url = url 20 | self.nombre = name 21 | self.pApellido = first_last_name 22 | self.sApellido = second_last_name 23 | self.rut = rut 24 | self.serviceId = service_id 25 | self.finalUrl = final_url 26 | self.commerceCode = commerce_code; 27 | self.montoMaximo = max_amount 28 | self.telefonoFijo = phone_number 29 | self.telefonoCelular = mobile_number 30 | self.nombrePatPass = patpass_name 31 | self.correoPersona = person_email 32 | self.correoComercio = commerce_email 33 | self.direccion = address 34 | self.ciudad = city 35 | 36 | 37 | class InscriptionStatusRequest(object): 38 | def __init__(self, 39 | token: str): 40 | self.token = token 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Transbank Developers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/request/__init__.py: -------------------------------------------------------------------------------- 1 | from transbank.validators.amount_validator import AmountValidator 2 | 3 | class TransactionCommitRequest(object): 4 | def __init__(self, id_query_installments: str, deferred_period_index: float, grace_period): 5 | self.id_query_installments = id_query_installments 6 | self.deferred_period_index = deferred_period_index 7 | if type(grace_period) == bool: 8 | grace_p = grace_period 9 | else: 10 | grace_p = grace_period == 'True' 11 | self.grace_period = str(grace_p).lower() 12 | 13 | 14 | class TransactionCreateRequest(object): 15 | def __init__(self, buy_order: str, session_id: str, amount: float, card_number: str, cvv: str, 16 | card_expiration_date: str): 17 | AmountValidator.validate(amount) 18 | self.buy_order = buy_order 19 | self.session_id = session_id 20 | self.amount = amount 21 | self.card_number = card_number 22 | self.cvv = cvv 23 | self.card_expiration_date = card_expiration_date 24 | 25 | class TransactionRefundRequest(object): 26 | def __init__(self, amount: float): 27 | self.amount = amount 28 | 29 | class TransactionCaptureRequest(object): 30 | def __init__(self, buy_order: str, authorization_code: str, capture_amount: float): 31 | self.buy_order = buy_order 32 | self.authorization_code = authorization_code 33 | self.capture_amount = capture_amount 34 | 35 | class TransactionInstallmentsRequest(object): 36 | def __init__(self, installments_number: float): 37 | self.installments_number = installments_number 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | .DS_Store 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | .vscode 110 | 111 | .idea 112 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/mall_bin_info.py: -------------------------------------------------------------------------------- 1 | from transbank.common.api_constants import ApiConstants 2 | from transbank.common.webpay_transaction import WebpayTransaction 3 | from transbank.common.validation_util import ValidationUtil 4 | from transbank.common.api_constants import ApiConstants 5 | from transbank.common.options import WebpayOptions 6 | from transbank.common.request_service import RequestService 7 | from transbank.error.transbank_error import TransbankError 8 | from transbank.error.mall_bin_info_query_error import MallBinInfoQueryError 9 | from transbank.webpay.oneclick.request import MallBinInfoQueryRequest 10 | from transbank.webpay.oneclick.schema import MallBinInfoQueryRequestSchema 11 | 12 | class MallBinInfo(WebpayTransaction): 13 | INFO_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/bin_info' 14 | 15 | def __init__(self, options: WebpayOptions): 16 | super().__init__(options) 17 | 18 | def query_bin(self, tbk_user: str): 19 | """ 20 | Queries the BIN information for a given `tbk_user`. 21 | 22 | Args: 23 | tbk_user (str): The `tbk_user` for which to query the BIN information. 24 | Returns: 25 | dict: The BIN information for the specified `tbk_user`. 26 | Raises: 27 | MallBinInfoQueryError: If there is an error querying the BIN information. 28 | TransbankError: If `tbk_user` exceeds the max length 29 | """ 30 | ValidationUtil.has_text_with_max_length(tbk_user, ApiConstants.TBK_USER_LENGTH, "tbk_user") 31 | try: 32 | endpoint = MallBinInfo.INFO_ENDPOINT 33 | request = MallBinInfoQueryRequest(tbk_user) 34 | return RequestService.post(endpoint, MallBinInfoQueryRequestSchema().dumps(request), self.options) 35 | except TransbankError as e: 36 | raise MallBinInfoQueryError(e.message, e.code) 37 | -------------------------------------------------------------------------------- /transbank/common/options.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from transbank.common.integration_type import IntegrationType 4 | 5 | 6 | class Options(ABC): 7 | def __init__(self, commerce_code: str, api_key: str, integration_type: IntegrationType, timeout: int = 600): 8 | self._commerce_code = commerce_code 9 | self._api_key = api_key 10 | self._integration_type = integration_type 11 | self.timeout = timeout 12 | 13 | @abstractmethod 14 | def header_commerce_code_name(self): 15 | pass 16 | 17 | @abstractmethod 18 | def header_api_key_name(self): 19 | pass 20 | 21 | @property 22 | def commerce_code(self) -> str: 23 | return self._commerce_code 24 | 25 | @property 26 | def integration_type(self) -> IntegrationType: 27 | return self._integration_type 28 | 29 | @property 30 | def api_key(self) -> str: 31 | return self._api_key 32 | 33 | @property 34 | def timeout(self) -> int: 35 | return self._timeout 36 | 37 | @timeout.setter 38 | def timeout(self, timeout: int) -> None: 39 | self._timeout = timeout 40 | 41 | @staticmethod 42 | def is_empty(options: 'Options') -> bool: 43 | return options is None or not options.commerce_code and not options.api_key and not options.integration_type 44 | 45 | def __repr__(self) -> str: 46 | return "Options(commerce_code: {}, api_key: {}, integration_type: {}, timeout: {})".format(self.commerce_code, self.api_key, self.integration_type, self.timeout) 47 | 48 | 49 | class WebpayOptions(Options): 50 | def header_commerce_code_name(self): 51 | return "Tbk-Api-Key-Id" 52 | 53 | def header_api_key_name(self): 54 | return "Tbk-Api-Key-Secret" 55 | 56 | 57 | class PatpassComercioOptions(Options): 58 | def header_commerce_code_name(self): 59 | return "commercecode" 60 | 61 | def header_api_key_name(self): 62 | return "Authorization" 63 | -------------------------------------------------------------------------------- /tests/webpay/transaccion_completa/test_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | from unittest.mock import patch 4 | import json 5 | from tests.mocks.transaccion_completa_responses_api_mocks import responses 6 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 7 | from transbank.common.integration_api_keys import IntegrationApiKeys 8 | from transbank.webpay.transaccion_completa.transaction import Transaction 9 | 10 | class TransactionTestCase(unittest.TestCase): 11 | 12 | def setUp(self) -> None: 13 | self.buy_order_mock = 'buy_order_mock_123456789' 14 | self.session_id_mock = 'session_ide_mock_123456789' 15 | self.amount_mock = 150000 16 | self.token_mock = '01abf2be20aad1da804aeae1ed3062fb8fba108ee0e07f4d37181f51c3f6714d' 17 | self.invalid_amount = -1000 18 | self.authorization_code_mock = '123456' 19 | self.capture_amount_mock = 150000 20 | self.cvv = '123' 21 | self.card_number = 'XXXXXXXXXXXX6623' 22 | self.card_expiration_date = '12/28' 23 | self.mock_response = Mock() 24 | self.transaction = Transaction.build_for_integration(IntegrationCommerceCodes.TRANSACCION_COMPLETA, IntegrationApiKeys.WEBPAY) 25 | 26 | @patch('transbank.common.request_service.requests.post') 27 | def test_create_transaction_successful(self, mock_post): 28 | self.mock_response.status_code = 200 29 | self.mock_response.text = json.dumps(responses['create_response']) 30 | mock_post.return_value = self.mock_response 31 | 32 | response = self.transaction.create(self.buy_order_mock, 33 | self.session_id_mock, 34 | self.amount_mock, 35 | self.cvv, 36 | self.card_number, 37 | self.card_expiration_date 38 | ) 39 | 40 | self.assertEqual(response, responses['create_response']) 41 | 42 | -------------------------------------------------------------------------------- /transbank/common/integration_commerce_codes.py: -------------------------------------------------------------------------------- 1 | # Contains the Webpay, Oneclick and Patpass Comercio constants for testing. 2 | class IntegrationCommerceCodes(object): 3 | WEBPAY_PLUS = "597055555532" 4 | WEBPAY_PLUS_MODAL = "597055555584" 5 | WEBPAY_PLUS_DEFERRED = "597055555540" 6 | WEBPAY_PLUS_MALL = "597055555535" 7 | WEBPAY_PLUS_MALL_CHILD1 = "597055555536" 8 | WEBPAY_PLUS_MALL_CHILD2 = "597055555537" 9 | WEBPAY_PLUS_MALL_CHILD_COMMERCE_CODES = ['597055555536', '597055555537'] 10 | WEBPAY_PLUS_MALL_DEFERRED = "597055555581" 11 | WEBPAY_PLUS_MALL_DEFERRED_CHILD1 = "597055555582" 12 | WEBPAY_PLUS_MALL_DEFERRED_CHILD2 = "597055555583" 13 | WEBPAY_PLUS_MALL_DEFERRED_CHILD_COMMERCE_CODES = ['597055555582', '597055555583'] 14 | ONECLICK_MALL = "597055555541" 15 | ONECLICK_MALL_CHILD1 = "597055555542" 16 | ONECLICK_MALL_CHILD2 = "597055555543" 17 | ONECLICK_MALL_DEFERRED = "597055555547" 18 | ONECLICK_MALL_DEFERRED_CHILD1 = "597055555548" 19 | ONECLICK_MALL_DEFERRED_CHILD2 = "597055555549" 20 | TRANSACCION_COMPLETA = "597055555530" 21 | TRANSACCION_COMPLETA_SIN_CVV = "597055555557" 22 | TRANSACCION_COMPLETA_DEFERRED = "597055555531" 23 | TRANSACCION_COMPLETA_DEFERRED_SIN_CVV = "597055555556" 24 | TRANSACCION_COMPLETA_MALL = "597055555573" 25 | TRANSACCION_COMPLETA_MALL_CHILD_COMMERCE_CODES = ['597055555574', '597055555575'] 26 | TRANSACCION_COMPLETA_MALL_CHILD1 = "597055555574" 27 | TRANSACCION_COMPLETA_MALL_CHILD2 = "597055555575" 28 | TRANSACCION_COMPLETA_MALL_SIN_CVV = "597055555551" 29 | TRANSACCION_COMPLETA_MALL_SIN_CVV_CHILD1 = "597055555552" 30 | TRANSACCION_COMPLETA_MALL_SIN_CVV_CHILD2 = "597055555553" 31 | TRANSACCION_COMPLETA_MALL_DEFERRED = "597055555576" 32 | TRANSACCION_COMPLETA_MALL_DEFERRED_CHILD1 = "597055555577" 33 | TRANSACCION_COMPLETA_MALL_DEFERRED_CHILD2 = "597055555578" 34 | TRANSACCION_COMPLETA_MALL_DEFERRED_SIN_CVV = "597055555561" 35 | TRANSACCION_COMPLETA_MALL_DEFERRED_SIN_CVV_CHILD1 = "597055555562" 36 | TRANSACCION_COMPLETA_MALL_DEFERRED_SIN_CVV_CHILD2 = "597055555563" 37 | PATPASS_COMERCIO = "28299257" 38 | 39 | -------------------------------------------------------------------------------- /tests/webpay/oneclick/test_mall_bin_info.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | from unittest.mock import Mock 4 | from unittest.mock import patch 5 | from transbank.error.transbank_error import TransbankError 6 | from transbank.error.mall_bin_info_query_error import MallBinInfoQueryError 7 | from transbank.webpay.oneclick.mall_bin_info import MallBinInfo 8 | 9 | 10 | class MallBinInfoTestCase(unittest.TestCase): 11 | 12 | def setUp(self) -> None: 13 | self.mock_response = Mock() 14 | 15 | @patch('transbank.common.request_service.requests.post') 16 | def test_query_bin(self, mock_post): 17 | response = {'bin_issuer': 'TEST COMMERCE BANK', 'bin_payment_type': 'Credito', 'bin_brand': 'Visa'} 18 | self.mock_response.status_code = 200 19 | self.mock_response.text = json.dumps(response) 20 | mock_post.return_value = self.mock_response 21 | 22 | mall_bin_info = MallBinInfo.build_for_integration('commerce_code', 'api_key') 23 | result = mall_bin_info.query_bin('tbkUser') 24 | 25 | _, kwargs = mock_post.call_args 26 | body = json.loads(kwargs['data']) 27 | 28 | self.assertEqual(result['bin_issuer'], 'TEST COMMERCE BANK') 29 | self.assertEqual(result['bin_payment_type'], 'Credito') 30 | self.assertEqual(result['bin_brand'], 'Visa') 31 | self.assertEqual(body['tbk_user'], 'tbkUser') 32 | 33 | def test_query_bin_invalid_tbk_user(self): 34 | mall_bin_info = MallBinInfo.build_for_integration('commerce_code', 'api_key') 35 | with self.assertRaises(TransbankError): 36 | mall_bin_info.query_bin('b134e1c5e9eeb134e1c5e9eeb134e1c5e9eeb134e1c5e9eeb134e1c5e9eeb134e1c5e9eeb134e1c5e9eeb134e1c5e9ee') 37 | 38 | @patch('transbank.common.request_service.requests.post') 39 | def test_query_bin_throws_api_exception(self, mock_post): 40 | self.mock_response.status_code = 400 41 | self.mock_response.text = '{"error": "Bad Request"}' 42 | mock_post.return_value = self.mock_response 43 | mall_bin_info = MallBinInfo.build_for_integration('commerce_code', 'api_key') 44 | 45 | with self.assertRaises(MallBinInfoQueryError): 46 | mall_bin_info.query_bin('tbkUser') 47 | -------------------------------------------------------------------------------- /tests/webpay/transaccion_completa/test_mall_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | from unittest.mock import patch 4 | import json 5 | from tests.mocks.transaccion_completa_responses_api_mocks import responses 6 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 7 | from transbank.common.integration_api_keys import IntegrationApiKeys 8 | from transbank.webpay.transaccion_completa.mall_transaction import MallTransaction 9 | 10 | class TransactionMallTestCase(unittest.TestCase): 11 | 12 | def setUp(self) -> None: 13 | self.buy_order_mock = 'buy_order_mock_123456789' 14 | self.session_id_mock = 'session_ide_mock_123456789' 15 | self.amount_mock = 150000 16 | self.token_mock = '01abf2be20aad1da804aeae1ed3062fb8fba108ee0e07f4d37181f51c3f6714d' 17 | self.invalid_amount = -1000 18 | self.authorization_code_mock = '123456' 19 | self.capture_amount_mock = 150000 20 | self.cvv = '123' 21 | self.card_number = 'XXXXXXXXXXXX6623' 22 | self.card_expiration_date = '12/28' 23 | self.details = [{"commerce_code": "commerce123", "buy_order": "order123", "amount": 1000, "installments_number": 1}] 24 | self.mock_response = Mock() 25 | self.transaction = MallTransaction.build_for_integration(IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL, IntegrationApiKeys.WEBPAY) 26 | 27 | @patch('transbank.common.request_service.requests.post') 28 | def test_create_transaction_successful(self, mock_post): 29 | self.mock_response.status_code = 200 30 | self.mock_response.text = json.dumps(responses['create_response']) 31 | mock_post.return_value = self.mock_response 32 | 33 | response = self.transaction.create(self.buy_order_mock, 34 | self.session_id_mock, 35 | self.card_number, 36 | self.card_expiration_date, 37 | self.details, 38 | self.cvv 39 | ) 40 | 41 | self.assertEqual(response, responses['create_response']) 42 | 43 | -------------------------------------------------------------------------------- /tests/patpass_comercio/test_inscription.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | from unittest.mock import Mock 4 | from tests.mocks.patpass_responses_api_mocks import responses 5 | from unittest.mock import patch 6 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 7 | from transbank.common.integration_api_keys import IntegrationApiKeys 8 | from transbank.patpass_comercio.inscription import Inscription 9 | 10 | class OneclickMallInscriptionTestCase(unittest.TestCase): 11 | 12 | def setUp(self) -> None: 13 | self.mock_response = Mock() 14 | self.inscription = Inscription.build_for_integration(IntegrationCommerceCodes.PATPASS_COMERCIO, IntegrationApiKeys.PATPASS_COMERCIO) 15 | 16 | @patch('transbank.common.request_service.requests.post') 17 | def test_inscription_start_transaction_successful(self, mock_post): 18 | self.mock_response.status_code = 200 19 | self.mock_response.text = json.dumps(responses['inscription_response']) 20 | mock_post.return_value = self.mock_response 21 | 22 | response = self.inscription.start( 23 | url="https://example.com", 24 | name="John", 25 | last_name="Doe", 26 | second_last_name="Smith", 27 | rut="12345678-9", 28 | service_id="service_001", 29 | final_url="https://example.com/final", 30 | max_amount=1000.0, 31 | phone="12345678", 32 | cell_phone="87654321", 33 | patpass_name="Patpass User", 34 | person_email="user@example.com", 35 | commerce_email="commerce@example.com", 36 | address="123 Street", 37 | city="Santiago" 38 | ) 39 | self.assertEqual(response, responses['inscription_response']) 40 | 41 | @patch('transbank.common.request_service.requests.post') 42 | def test_status_successful(self, mock_post): 43 | self.mock_response.status_code = 200 44 | self.mock_response.text = json.dumps(responses['status_response']) 45 | mock_post.return_value = self.mock_response 46 | 47 | response = self.inscription.status("dummy_token") 48 | 49 | self.assertIn("authorized", response) 50 | self.assertEqual(response, responses['status_response']) 51 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/mall_request/__init__.py: -------------------------------------------------------------------------------- 1 | class TransactionCommitRequest(object): 2 | def __init__(self, details: list): 3 | self.details = self.commit_details(details) 4 | 5 | def commit_details(self, details: list) -> list: 6 | return [{ 7 | "commerce_code": detail['commerce_code'], 8 | "buy_order": detail['buy_order'], 9 | "id_query_installments": detail['id_query_installments'], 10 | "deferred_period_index": detail['deferred_period_index'], 11 | "grace_period": detail['grace_period'] 12 | } for detail in details] 13 | 14 | 15 | class TransactionCreateRequest(object): 16 | def __init__(self, buy_order: str, session_id: str, card_number: str, card_expiration_date: str, details: list, cvv: str): 17 | self.buy_order = buy_order 18 | self.session_id = session_id 19 | self.card_number = card_number 20 | self.card_expiration_date = card_expiration_date 21 | self.details = self.create_details(details) 22 | self.cvv = cvv 23 | 24 | def create_details(self, details: list) -> list: 25 | return [ 26 | { 27 | "amount": detail["amount"], 28 | "commerce_code": detail["commerce_code"], 29 | "buy_order": detail["buy_order"] 30 | } for detail in details 31 | ] 32 | 33 | 34 | class TransactionStatusRequest(object): 35 | def __init__(self, token: str): 36 | self.token = token 37 | 38 | 39 | class TransactionRefundRequest(object): 40 | def __init__(self, commerce_code: str, buy_order: str, amount: float): 41 | self.buy_order = buy_order 42 | self.commerce_code = commerce_code 43 | self.amount = amount 44 | 45 | 46 | class TransactionCaptureRequest(object): 47 | def __init__(self, commerce_code: str, buy_order: str, authorization_code: str, capture_amount: float): 48 | self.commerce_code = commerce_code 49 | self.buy_order= buy_order 50 | self.authorization_code = authorization_code 51 | self.capture_amount = capture_amount 52 | 53 | 54 | class TransactionInstallmentsRequest(object): 55 | def __init__(self, installments_number: float, buy_order: str, commerce_code: str): 56 | self.installments_number = installments_number 57 | self.buy_order = buy_order 58 | self.commerce_code = commerce_code 59 | -------------------------------------------------------------------------------- /transbank/common/request_service.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from transbank.common.options import Options, WebpayOptions 4 | from transbank.common.headers_builder import HeadersBuilder 5 | from transbank.common.integration_type import webpay_host, patpass_comercio_host 6 | from transbank.error.transbank_error import TransbankError 7 | 8 | class RequestService(object): 9 | 10 | @classmethod 11 | def post(cls, endpoint: str, request: str, options: Options): 12 | endpoint = "{}{}".format(cls.host(options), endpoint) 13 | response = requests.post(url=endpoint, data=request, headers=HeadersBuilder.build(options), timeout=options.timeout) 14 | return cls.process_response(response) 15 | 16 | @classmethod 17 | def delete(cls, endpoint: str, request: str, options: Options): 18 | endpoint = "{}{}".format(cls.host(options), endpoint) 19 | response = requests.delete(url=endpoint, data=request, headers=HeadersBuilder.build(options), timeout=options.timeout) 20 | return cls.process_response(response) 21 | 22 | @classmethod 23 | def put(cls, endpoint: str, request: str, options: Options): 24 | endpoint = "{}{}".format(cls.host(options), endpoint) 25 | response = requests.put(url=endpoint, data=request, headers=HeadersBuilder.build(options), timeout=options.timeout) 26 | return cls.process_response(response) 27 | 28 | @classmethod 29 | def get(cls, endpoint: str, options: Options): 30 | endpoint = "{}{}".format(cls.host(options), endpoint) 31 | response = requests.get(url=endpoint, headers=HeadersBuilder.build(options), timeout=options.timeout) 32 | return cls.process_response(response) 33 | 34 | @classmethod 35 | def process_response(cls, response: any): 36 | if not response.text: 37 | return response.status_code 38 | dict_response = json.loads(response.text) 39 | if response.status_code not in (200, 299): 40 | if "error_message" in dict_response: 41 | raise TransbankError(message=dict_response["error_message"], code=response.status_code) 42 | if "description" in dict_response: 43 | raise TransbankError(message=dict_response["description"], code=response.status_code) 44 | raise TransbankError(message=response.text, code=response.status_code) 45 | return dict_response 46 | 47 | @classmethod 48 | def host(cls, options: Options): 49 | if isinstance(options, WebpayOptions): 50 | return webpay_host(options.integration_type) 51 | else: 52 | return patpass_comercio_host(options.integration_type) 53 | -------------------------------------------------------------------------------- /transbank/patpass_comercio/inscription.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import PatpassComercioOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.integration_type import IntegrationType 5 | from transbank.patpass_comercio.request import InscriptionStartRequest, InscriptionStatusRequest 6 | from transbank.patpass_comercio.schema import InscriptionStartRequestSchema, InscriptionStatusRequestSchema 7 | from transbank.error.transbank_error import TransbankError 8 | from transbank.error.inscription_start_error import InscriptionStartError 9 | from transbank.error.inscription_status_error import InscriptionStatusError 10 | 11 | class Inscription(object): 12 | 13 | START_ENDPOINT = ApiConstants.PATPASS_ENDPOINT + '/patInscription' 14 | STATUS_ENDPOINT = ApiConstants.PATPASS_ENDPOINT + '/status' 15 | 16 | def __init__(self, options: PatpassComercioOptions): 17 | self.options = options 18 | 19 | @classmethod 20 | def build_for_integration(cls, commerce_code, api_key): 21 | options = PatpassComercioOptions(commerce_code, api_key, IntegrationType.TEST) 22 | return cls(options) 23 | 24 | @classmethod 25 | def build_for_production(cls, commerce_code, api_key): 26 | options = PatpassComercioOptions(commerce_code, api_key, IntegrationType.LIVE) 27 | return cls(options) 28 | 29 | def start(self, url: str, 30 | name: str, 31 | last_name: str, 32 | second_last_name: str, 33 | rut: str, 34 | service_id: str, 35 | final_url: str, 36 | max_amount: float, 37 | phone: str, 38 | cell_phone: str, 39 | patpass_name: str, 40 | person_email: str, 41 | commerce_email: str, 42 | address: str, 43 | city: str): 44 | try: 45 | m_amount = max_amount 46 | if max_amount == 0: 47 | m_amount = '' 48 | endpoint = Inscription.START_ENDPOINT 49 | request = InscriptionStartRequest(url, name, last_name, second_last_name, rut, 50 | service_id, final_url, self.options.commerce_code, m_amount, 51 | phone, cell_phone, patpass_name, person_email, 52 | commerce_email, address, city) 53 | return RequestService.post(endpoint, InscriptionStartRequestSchema().dumps(request), self.options) 54 | except TransbankError as e: 55 | raise InscriptionStartError(e.message, e.code) 56 | 57 | def status(self, token: str): 58 | try: 59 | endpoint = Inscription.STATUS_ENDPOINT 60 | request = InscriptionStatusRequest(token) 61 | return RequestService.post(endpoint, InscriptionStatusRequestSchema().dumps(request), self.options) 62 | except TransbankError as e: 63 | raise InscriptionStatusError(e.message, e.code) 64 | 65 | 66 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/mall_inscription.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.webpay_transaction import WebpayTransaction 5 | from transbank.common.validation_util import ValidationUtil 6 | from transbank.webpay.oneclick.schema import MallInscriptionStartRequestSchema, MallInscriptionDeleteRequestSchema 7 | from transbank.webpay.oneclick.request import MallInscriptionStartRequest, MallInscriptionDeleteRequest 8 | from transbank.error.transbank_error import TransbankError 9 | from transbank.error.inscription_start_error import InscriptionStartError 10 | from transbank.error.inscription_finish_error import InscriptionFinishError 11 | from transbank.error.inscription_delete_error import InscriptionDeleteError 12 | 13 | 14 | class MallInscription(WebpayTransaction): 15 | START_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/inscriptions' 16 | FINISH_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/inscriptions/{}' 17 | DELETE_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/inscriptions' 18 | 19 | def __init__(self, options: WebpayOptions): 20 | super().__init__(options) 21 | 22 | def start(self, username: str, email: str, response_url: str): 23 | ValidationUtil.has_text_trim_with_max_length(username, ApiConstants.USER_NAME_LENGTH, "username") 24 | ValidationUtil.has_text_trim_with_max_length(email, ApiConstants.EMAIL_LENGTH, "email") 25 | ValidationUtil.has_text_with_max_length(response_url, ApiConstants.RETURN_URL_LENGTH, "response_url") 26 | try: 27 | endpoint = MallInscription.START_ENDPOINT 28 | request = MallInscriptionStartRequest(username, email, response_url) 29 | return RequestService.post(endpoint, MallInscriptionStartRequestSchema().dumps(request), self.options) 30 | except TransbankError as e: 31 | raise InscriptionStartError(e.message, e.code) 32 | 33 | def finish(self, token: str): 34 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 35 | try: 36 | endpoint = MallInscription.FINISH_ENDPOINT.format(token) 37 | return RequestService.put(endpoint, {}, self.options) 38 | except TransbankError as e: 39 | raise InscriptionFinishError(e.message, e.code) 40 | 41 | def delete(self, tbk_user: str, username: str): 42 | ValidationUtil.has_text_trim_with_max_length(username, ApiConstants.USER_NAME_LENGTH, "username") 43 | ValidationUtil.has_text_with_max_length(tbk_user, ApiConstants.TBK_USER_LENGTH, "tbk_user") 44 | try: 45 | endpoint = MallInscription.DELETE_ENDPOINT 46 | request = MallInscriptionDeleteRequest(username, tbk_user) 47 | response = RequestService.delete(endpoint, MallInscriptionDeleteRequestSchema().dumps(request), self.options) 48 | return response == ApiConstants.HTTP_STATUS_DELETE_OK 49 | 50 | except TransbankError as e: 51 | raise InscriptionDeleteError(e.message, e.code) 52 | 53 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/request/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Iterator 2 | from transbank.validators.amount_validator import AmountValidator 3 | 4 | 5 | class MallInscriptionStartRequest(object): 6 | def __init__(self, 7 | user_name: str, 8 | email: str, 9 | response_url: str): 10 | self.username = user_name 11 | self.email = email 12 | self.response_url = response_url 13 | 14 | 15 | class MallInscriptionDeleteRequest(object): 16 | def __init__(self, 17 | user_name: str, 18 | tbk_user: str): 19 | self.username = user_name 20 | self.tbk_user = tbk_user 21 | 22 | 23 | class MallDetails(object): 24 | def __init__(self, commerce_code: str, buy_order: str, installments_number: int, amount: float): 25 | AmountValidator.validate(amount) 26 | self.commerce_code = commerce_code 27 | self.buy_order = buy_order 28 | self.installments_number = installments_number 29 | self.amount = amount 30 | 31 | def __repr__(self): 32 | return "MallDetails(commerce_code: {}, buy_order: {}, installments_number: {}, amount: {})".format( 33 | self.commerce_code, self.buy_order, self.installments_number, self.amount) 34 | 35 | def __eq__(self, other) -> bool: 36 | if type(other) is not MallTransactionAuthorizeDetails: 37 | return False 38 | 39 | return self.commerce_code == other.commerce_code \ 40 | and self.buy_order == other.buy_order \ 41 | and self.installments_number == other.installments_number \ 42 | and self.amount == other.amount 43 | 44 | class MallTransactionAuthorizeDetails(object): 45 | def __init__(self, commerce_code: str, buy_order: str, installments_number: int, amount: float): 46 | self.__details = [] 47 | self.add(commerce_code, buy_order, installments_number, amount) 48 | 49 | def add(self, commerce_code: str, buy_order: str, installments_number: int, 50 | amount: float) -> "MallTransactionAuthorizeDetails": 51 | mall_details = MallDetails(commerce_code, buy_order, installments_number, amount) 52 | self.__details.append(mall_details) 53 | return self 54 | 55 | def remove(self, commerce_code: str, buy_order: str, installments_number: int, amount: float) -> None: 56 | mall_details = MallDetails(commerce_code, buy_order, installments_number, amount) 57 | self.__details.remove(mall_details) 58 | 59 | @property 60 | def details(self) -> Iterator[MallDetails]: 61 | return tuple(self.__details) 62 | 63 | 64 | 65 | 66 | 67 | class MallTransactionAuthorizeRequest(object): 68 | def __init__(self, 69 | username: str, 70 | tbk_user: str, 71 | buy_order: str, 72 | details: List[MallDetails]): 73 | self.username = username 74 | self.tbk_user = tbk_user 75 | self.buy_order = buy_order 76 | self.details = details 77 | 78 | 79 | class MallTransactionCaptureRequest(object): 80 | def __init__(self, commerce_code: str, buy_order: str, authorization_code: str, capture_amount: float): 81 | self.commerce_code = commerce_code 82 | self.buy_order = buy_order 83 | self.authorization_code = authorization_code 84 | self.capture_amount = capture_amount 85 | 86 | def __repr__(self): 87 | return "MallTransactionCaptureRequest(commerce_code: {}, buy_order: {}, authorization_code: {}, capture_amount: {})".format( 88 | self.commerce_code, self.buy_order, self.authorization_code, self.capture_amount ) 89 | 90 | 91 | class MallTransactionRefundRequest(object): 92 | def __init__(self, 93 | commerce_code: str, 94 | detail_buy_order: str, 95 | amount: float): 96 | AmountValidator.validate(amount) 97 | self.commerce_code = commerce_code 98 | self.detail_buy_order = detail_buy_order 99 | self.amount = amount 100 | 101 | class MallBinInfoQueryRequest(object): 102 | def __init__(self, tbk_user: str): 103 | self.tbk_user = tbk_user 104 | 105 | def __repr__(self): 106 | return "MallBinInfoQueryRequest(tbk_user: {})".format(self.tbk_user) 107 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | import subprocess 11 | from shutil import rmtree 12 | 13 | from setuptools import find_packages, setup, Command 14 | 15 | # Package meta-data. 16 | 17 | NAME = 'transbank-sdk' 18 | MODULE_NAME = 'transbank' 19 | DESCRIPTION = 'Transbank Python SDK' 20 | URL = 'https://github.com/TransbankDevelopers/transbank-sdk-python' 21 | EMAIL = 'transbankdevelopers@continuum.cl' 22 | AUTHOR = 'Transbank' 23 | VERSION = None 24 | 25 | # What packages are required for this module to be executed? 26 | REQUIRED = [ 27 | "marshmallow>3, <=3.26.1", 28 | "requests>=2.20.0" 29 | ] 30 | 31 | TESTS_REQUIREMENTS = [ 32 | "pytest", 33 | "coverage", 34 | "mock", 35 | "requests-mock<=1.5.2" 36 | ] 37 | 38 | # The rest you shouldn't have to touch too much :) 39 | # ------------------------------------------------ 40 | # Except, perhaps the License and Trove Classifiers! 41 | # If you do change the License, remember to change the Trove Classifier for that! 42 | 43 | here = os.path.abspath(os.path.dirname(__file__)) 44 | 45 | # Import the README and use it as the long-description. 46 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 47 | try: 48 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 49 | long_description = '\n' + f.read() 50 | except FileNotFoundError: 51 | long_description = DESCRIPTION 52 | 53 | # Load the package's __version__.py module as a dictionary. 54 | about = {} 55 | if not VERSION: 56 | with open(os.path.join(here, MODULE_NAME, '__version__.py')) as f: 57 | exec(f.read(), about) 58 | else: 59 | about['__version__'] = VERSION 60 | 61 | class UploadCommand(Command): 62 | """Support setup.py upload.""" 63 | 64 | description = 'Build and publish the package.' 65 | user_options = [] 66 | 67 | @staticmethod 68 | def status(s): 69 | """Prints things in bold.""" 70 | print('\033[1m{0}\033[0m'.format(s)) 71 | 72 | def initialize_options(self): 73 | pass 74 | 75 | def finalize_options(self): 76 | pass 77 | 78 | def run(self): 79 | try: 80 | self.status('Removing previous builds…') 81 | rmtree(os.path.join(here, 'dist')) 82 | except FileNotFoundError: 83 | pass 84 | 85 | self.status('Building Source and Wheel (universal) distribution…') 86 | subprocess.run([sys.executable, "setup.py", "sdist", "bdist_wheel", "--universal"], check=True) 87 | 88 | self.status('Uploading the package to PyPI via Twine…') 89 | subprocess.run(["twine", "upload", "dist/*"], check=True) 90 | 91 | self.status('Pushing git tags…') 92 | subprocess.run(["git", "tag", f"v{about['__version__']}"], check=True) 93 | subprocess.run(["git", "push", "--tags"], check=True) 94 | 95 | sys.exit() 96 | 97 | # Where the magic happens: 98 | setup( 99 | name=NAME, 100 | version=about['__version__'], 101 | description=DESCRIPTION, 102 | long_description=long_description, 103 | long_description_content_type='text/markdown', 104 | author=AUTHOR, 105 | author_email=EMAIL, 106 | url=URL, 107 | packages=find_packages(exclude=('tests',)), 108 | install_requires=REQUIRED, 109 | tests_require=TESTS_REQUIREMENTS, 110 | include_package_data=True, 111 | license='BSD 3-clause "New" or "Revised License"', 112 | classifiers=[ 113 | # Trove classifiers 114 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 115 | 'License :: OSI Approved :: BSD License', 116 | 'Programming Language :: Python', 117 | "Programming Language :: Python :: 3", 118 | "Programming Language :: Python :: 3.8", 119 | "Programming Language :: Python :: 3.9", 120 | "Programming Language :: Python :: 3.10", 121 | "Programming Language :: Python :: 3.11", 122 | "Programming Language :: Python :: 3.12", 123 | 'Programming Language :: Python :: Implementation :: CPython', 124 | ], 125 | # $ setup.py publish support. 126 | cmdclass={ 127 | 'upload': UploadCommand, 128 | }, 129 | ) 130 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/request/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any, Iterator, Tuple 2 | 3 | 4 | class TransactionCreateRequest(object): 5 | def __init__(self, buy_order: str, session_id: str, amount: float, return_url: str): 6 | self.buy_order = buy_order 7 | self.session_id = session_id 8 | self.amount = amount 9 | self.return_url = return_url 10 | 11 | def __repr__(self): 12 | return "TransactionCreateRequest(buy_order: {}, session_id: {}, amount: {}, return_url: {})".format( 13 | self.buy_order, self.session_id, self.amount, self.return_url) 14 | 15 | 16 | class TransactionRefundRequest(object): 17 | def __init__(self, amount: float): 18 | self.amount = amount 19 | 20 | def __repr__(self): 21 | return "TransactionRefundRequest(amount: {})".format(self.amount) 22 | 23 | class TransactionCaptureRequest(object): 24 | def __init__(self, buy_order: str, authorization_code: str, capture_amount: float): 25 | self.buy_order = buy_order 26 | self.authorization_code = authorization_code 27 | self.capture_amount = capture_amount 28 | 29 | def __repr__(self): 30 | return "TransactionCaptureRequest(buy_order: {}, authorization_code: {}, capture_amount: {})".format( 31 | self.buy_order, self.authorization_code, self.capture_amount) 32 | 33 | class MallTransactionRefundRequest(object): 34 | def __init__(self, commerce_code: str, buy_order: str, amount: float): 35 | self.buy_order = buy_order 36 | self.commerce_code = commerce_code 37 | self.amount = amount 38 | 39 | def __repr__(self): 40 | return "MallTransactionRefundRequest(amount: {}, buy_order: {}, commerce_code: {})".format( 41 | self.amount, self.buy_order, self.commerce_code) 42 | 43 | class MallDetails(object): 44 | def __init__(self, amount: float, commerce_code: str, buy_order: str): 45 | self.amount = amount 46 | self.commerce_code = commerce_code 47 | self.buy_order = buy_order 48 | 49 | def __repr__(self): 50 | return "MallDetails(amount: {}, commerce_code: {}, buy_order: {})".format( 51 | self.amount, self.commerce_code, self.buy_order) 52 | 53 | def __eq__(self, other) -> bool: 54 | if type(other) is not MallTransactionCreateDetails: 55 | return False 56 | 57 | return self.amount == other.amount and self.commerce_code == other.commerce_code \ 58 | and self.buy_order == other.buy_order 59 | 60 | 61 | class MallTransactionCreateDetails(object): 62 | __details = [] 63 | 64 | def __init__(self, amount: float, commerce_code: str, buy_order: str): 65 | self.clean() 66 | self.add(amount, commerce_code, buy_order) 67 | 68 | def clean(self): 69 | self.__details = [] 70 | 71 | def add(self, amount: float, commerce_code: str, buy_order: str) -> "MallTransactionCreateDetails": 72 | mall_details = MallDetails(amount, commerce_code, buy_order) 73 | self.__details.append(mall_details) 74 | return self 75 | 76 | def remove(self, amount: float, commerce_code: str, buy_order: str) -> None: 77 | mall_details = MallDetails(amount, commerce_code, buy_order) 78 | self.__details.remove(mall_details) 79 | 80 | @property 81 | def details(self) -> Tuple[MallDetails]: 82 | return tuple(self.__details) 83 | 84 | 85 | class MallTransactionCreateRequest(object): 86 | def __init__(self, buy_order: str, session_id: str, return_url: str, details: Tuple[MallDetails]): 87 | self.buy_order = buy_order 88 | self.session_id = session_id 89 | self.return_url = return_url 90 | self.details = details 91 | 92 | 93 | class MallTransactionCaptureRequest(object): 94 | def __init__(self, commerce_code: str, buy_order: str, authorization_code: str, capture_amount: float): 95 | self.commerce_code = commerce_code 96 | self.buy_order = buy_order 97 | self.authorization_code = authorization_code 98 | self.capture_amount = capture_amount 99 | 100 | def __repr__(self): 101 | return "MallTransactionCaptureRequest(commerce_code: {}, buy_order: {}, authorization_code: {}, capture_amount: {})".format( 102 | self.commerce_code, self.buy_order, self.authorization_code, self.capture_amount ) 103 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.validation_util import ValidationUtil 5 | from transbank.common.webpay_transaction import WebpayTransaction 6 | from transbank.webpay.webpay_plus.schema import TransactionCreateRequestSchema, \ 7 | TransactionRefundRequestSchema, TransactionCaptureRequestSchema 8 | from transbank.webpay.webpay_plus.request import TransactionCreateRequest, \ 9 | TransactionRefundRequest, TransactionCaptureRequest 10 | from transbank.error.transbank_error import TransbankError 11 | from transbank.error.transaction_create_error import TransactionCreateError 12 | from transbank.error.transaction_commit_error import TransactionCommitError 13 | from transbank.error.transaction_status_error import TransactionStatusError 14 | from transbank.error.transaction_refund_error import TransactionRefundError 15 | from transbank.error.transaction_capture_error import TransactionCaptureError 16 | 17 | class Transaction(WebpayTransaction): 18 | CREATE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/' 19 | COMMIT_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 20 | STATUS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 21 | REFUND_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/refunds' 22 | CAPTURE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/capture' 23 | 24 | def __init__(self, options: WebpayOptions): 25 | super().__init__(options) 26 | 27 | def create(self, buy_order: str, session_id: str, amount: float, return_url: str): 28 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 29 | ValidationUtil.has_text_with_max_length(session_id, ApiConstants.SESSION_ID_LENGTH, "session_id") 30 | ValidationUtil.has_text_with_max_length(return_url, ApiConstants.RETURN_URL_LENGTH, "return_url") 31 | try: 32 | endpoint = Transaction.CREATE_ENDPOINT 33 | request = TransactionCreateRequest(buy_order, session_id, amount, return_url) 34 | return RequestService.post(endpoint, TransactionCreateRequestSchema().dumps(request), self.options) 35 | except TransbankError as e: 36 | raise TransactionCreateError(e.message, e.code) 37 | 38 | def commit(self, token: str): 39 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 40 | try: 41 | endpoint = Transaction.COMMIT_ENDPOINT.format(token) 42 | return RequestService.put(endpoint, {}, self.options) 43 | except TransbankError as e: 44 | raise TransactionCommitError(e.message, e.code) 45 | 46 | def status(self, token: str): 47 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 48 | try: 49 | endpoint = Transaction.STATUS_ENDPOINT.format(token) 50 | return RequestService.get(endpoint, self.options) 51 | except TransbankError as e: 52 | raise TransactionStatusError(e.message, e.code) 53 | 54 | def refund(self, token: str, amount: float): 55 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 56 | try: 57 | endpoint = Transaction.REFUND_ENDPOINT.format(token) 58 | request = TransactionRefundRequest(amount) 59 | return RequestService.post(endpoint, TransactionRefundRequestSchema().dumps(request), self.options) 60 | except TransbankError as e: 61 | raise TransactionRefundError(e.message, e.code) 62 | 63 | def capture(self, token: str, buy_order: str, authorization_code: str, capture_amount: float): 64 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 65 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 66 | ValidationUtil.has_text_with_max_length(authorization_code, ApiConstants.AUTHORIZATION_CODE_LENGTH, "authorization_code") 67 | try: 68 | endpoint = Transaction.CAPTURE_ENDPOINT.format(token) 69 | request = TransactionCaptureRequest(buy_order, authorization_code, capture_amount) 70 | return RequestService.put(endpoint, TransactionCaptureRequestSchema().dumps(request), self.options) 71 | except TransbankError as e: 72 | raise TransactionCaptureError(e.message, e.code) 73 | 74 | -------------------------------------------------------------------------------- /transbank/webpay/oneclick/mall_transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.webpay_transaction import WebpayTransaction 5 | from transbank.common.validation_util import ValidationUtil 6 | from transbank.webpay.oneclick.schema import MallTransactionAuthorizeRequestSchema, MallTransactionRefundRequestSchema, MallTransactionCaptureRequestSchema 7 | from transbank.webpay.oneclick.request import MallTransactionAuthorizeDetails, MallTransactionAuthorizeRequest, MallTransactionRefundRequest, MallTransactionCaptureRequest 8 | from transbank.error.transbank_error import TransbankError 9 | from transbank.error.transaction_authorize_error import TransactionAuthorizeError 10 | from transbank.error.transaction_status_error import TransactionStatusError 11 | from transbank.error.transaction_refund_error import TransactionRefundError 12 | from transbank.error.transaction_capture_error import TransactionCaptureError 13 | 14 | class MallTransaction(WebpayTransaction): 15 | AUTHORIZE_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/transactions' 16 | STATUS_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/transactions/{}' 17 | REFUND_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/transactions/{}/refunds' 18 | CAPTURE_ENDPOINT = ApiConstants.ONECLICK_ENDPOINT + '/transactions/capture' 19 | 20 | def __init__(self, options: WebpayOptions): 21 | super().__init__(options) 22 | 23 | def authorize(self, username: str, tbk_user: str, parent_buy_order: str, details: MallTransactionAuthorizeDetails): 24 | ValidationUtil.has_text_with_max_length(username, ApiConstants.USER_NAME_LENGTH, "username") 25 | ValidationUtil.has_text_with_max_length(tbk_user, ApiConstants.TBK_USER_LENGTH, "tbk_user") 26 | ValidationUtil.has_text_with_max_length(parent_buy_order, ApiConstants.BUY_ORDER_LENGTH, "parent_buy_order") 27 | ValidationUtil.has_elements(details.details, "details") 28 | 29 | for item in details.details: 30 | ValidationUtil.has_text_with_max_length(item.commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "details.commerce_code") 31 | ValidationUtil.has_text_with_max_length(item.buy_order, ApiConstants.BUY_ORDER_LENGTH, "details.buy_order") 32 | try: 33 | endpoint = MallTransaction.AUTHORIZE_ENDPOINT 34 | request = MallTransactionAuthorizeRequest(username, tbk_user, parent_buy_order, details.details) 35 | return RequestService.post(endpoint, MallTransactionAuthorizeRequestSchema().dumps(request), self.options) 36 | except TransbankError as e: 37 | raise TransactionAuthorizeError(e.message, e.code) 38 | 39 | def capture(self, child_commerce_code: str, child_buy_order: str, authorization_code: str, capture_amount: float): 40 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 41 | ValidationUtil.has_text_with_max_length(child_buy_order, ApiConstants.BUY_ORDER_LENGTH, "child_buy_order") 42 | ValidationUtil.has_text_with_max_length(authorization_code, ApiConstants.AUTHORIZATION_CODE_LENGTH, "authorization_code") 43 | try: 44 | endpoint = MallTransaction.CAPTURE_ENDPOINT 45 | request = MallTransactionCaptureRequest(child_commerce_code, child_buy_order, authorization_code, capture_amount) 46 | return RequestService.put(endpoint, MallTransactionCaptureRequestSchema().dumps(request), self.options) 47 | except TransbankError as e: 48 | raise TransactionCaptureError(e.message, e.code) 49 | 50 | def status(self, buy_order: str): 51 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 52 | try: 53 | endpoint = MallTransaction.STATUS_ENDPOINT.format(buy_order) 54 | return RequestService.get(endpoint, self.options) 55 | except TransbankError as e: 56 | raise TransactionStatusError(e.message, e.code) 57 | 58 | def refund(self, buy_order: str, child_commerce_code: str, child_buy_order: str, amount: float): 59 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 60 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 61 | ValidationUtil.has_text_with_max_length(child_buy_order, ApiConstants.BUY_ORDER_LENGTH, "child_buy_order") 62 | try: 63 | endpoint = MallTransaction.REFUND_ENDPOINT.format(buy_order) 64 | request = MallTransactionRefundRequest(child_commerce_code, child_buy_order, amount) 65 | return RequestService.post(endpoint, MallTransactionRefundRequestSchema().dumps(request), self.options) 66 | except TransbankError as e: 67 | raise TransactionRefundError(e.message, e.code) 68 | 69 | -------------------------------------------------------------------------------- /transbank/webpay/webpay_plus/mall_transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.webpay_transaction import WebpayTransaction 5 | from transbank.common.validation_util import ValidationUtil 6 | from transbank.webpay.webpay_plus.mall_schema import MallTransactionCreateRequestSchema, MallTransactionRefundRequestSchema, MallTransactionCaptureRequestSchema 7 | from transbank.webpay.webpay_plus.request import MallTransactionCreateDetails, MallTransactionCreateRequest, \ 8 | MallTransactionRefundRequest, MallTransactionCaptureRequest 9 | from transbank.error.transbank_error import TransbankError 10 | from transbank.error.transaction_create_error import TransactionCreateError 11 | from transbank.error.transaction_commit_error import TransactionCommitError 12 | from transbank.error.transaction_status_error import TransactionStatusError 13 | from transbank.error.transaction_refund_error import TransactionRefundError 14 | from transbank.error.transaction_capture_error import TransactionCaptureError 15 | 16 | class MallTransaction(WebpayTransaction): 17 | CREATE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/' 18 | COMMIT_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 19 | STATUS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 20 | REFUND_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/refunds' 21 | CAPTURE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/capture' 22 | 23 | def __init__(self, options: WebpayOptions): 24 | super().__init__(options) 25 | 26 | def create(self, buy_order: str, session_id: str, return_url: str, details: MallTransactionCreateDetails): 27 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 28 | ValidationUtil.has_text_with_max_length(session_id, ApiConstants.SESSION_ID_LENGTH, "session_id") 29 | ValidationUtil.has_text_with_max_length(return_url, ApiConstants.RETURN_URL_LENGTH, "return_url") 30 | ValidationUtil.has_elements(details.details, "details") 31 | 32 | for item in details.details: 33 | ValidationUtil.has_text_with_max_length(item.commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "details.commerce_code") 34 | ValidationUtil.has_text_with_max_length(item.buy_order, ApiConstants.BUY_ORDER_LENGTH, "details.buy_order") 35 | 36 | try: 37 | endpoint = MallTransaction.CREATE_ENDPOINT 38 | request = MallTransactionCreateRequest(buy_order, session_id, return_url, details.details) 39 | return RequestService.post(endpoint, MallTransactionCreateRequestSchema().dumps(request), self.options) 40 | except TransbankError as e: 41 | raise TransactionCreateError(e.message, e.code) 42 | 43 | def commit(self, token: str): 44 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 45 | try: 46 | endpoint = MallTransaction.COMMIT_ENDPOINT.format(token) 47 | return RequestService.put(endpoint, {}, self.options) 48 | except TransbankError as e: 49 | raise TransactionCommitError(e.message, e.code) 50 | 51 | def status(self, token: str): 52 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 53 | try: 54 | endpoint = MallTransaction.STATUS_ENDPOINT.format(token) 55 | return RequestService.get(endpoint, self.options) 56 | except TransbankError as e: 57 | raise TransactionStatusError(e.message, e.code) 58 | 59 | def refund(self, token: str, child_buy_order: str, child_commerce_code:str, amount: float): 60 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 61 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 62 | ValidationUtil.has_text_with_max_length(child_buy_order, ApiConstants.BUY_ORDER_LENGTH, "child_buy_order") 63 | try: 64 | endpoint = MallTransaction.REFUND_ENDPOINT.format(token) 65 | request = MallTransactionRefundRequest(commerce_code=child_commerce_code, buy_order=child_buy_order, amount=amount) 66 | return RequestService.post(endpoint, MallTransactionRefundRequestSchema().dumps(request), self.options) 67 | except TransbankError as e: 68 | raise TransactionRefundError(e.message, e.code) 69 | 70 | def capture(self, child_commerce_code: str, token: str, buy_order: str, authorization_code: str, capture_amount: float): 71 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 72 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 73 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 74 | ValidationUtil.has_text_with_max_length(authorization_code, ApiConstants.AUTHORIZATION_CODE_LENGTH, "authorization_code") 75 | try: 76 | endpoint = MallTransaction.CAPTURE_ENDPOINT.format(token) 77 | request = MallTransactionCaptureRequest(child_commerce_code, buy_order, authorization_code, capture_amount) 78 | return RequestService.put(endpoint, MallTransactionCaptureRequestSchema().dumps(request), self.options) 79 | except TransbankError as e: 80 | raise TransactionCaptureError(e.message, e.code) 81 | 82 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.webpay_transaction import WebpayTransaction 5 | from transbank.common.validation_util import ValidationUtil 6 | from transbank.webpay.transaccion_completa.request import TransactionCreateRequest, TransactionCommitRequest, \ 7 | TransactionRefundRequest, TransactionCaptureRequest, TransactionInstallmentsRequest 8 | from transbank.webpay.transaccion_completa.schema import TransactionCreateRequestSchema, \ 9 | TransactionCommitRequestSchema, TransactionInstallmentsRequestSchema, TransactionRefundRequestSchema, TransactionCaptureRequestSchema 10 | from transbank.error.transbank_error import TransbankError 11 | from transbank.error.transaction_create_error import TransactionCreateError 12 | from transbank.error.transaction_commit_error import TransactionCommitError 13 | from transbank.error.transaction_status_error import TransactionStatusError 14 | from transbank.error.transaction_refund_error import TransactionRefundError 15 | from transbank.error.transaction_capture_error import TransactionCaptureError 16 | from transbank.error.transaction_installments_error import TransactionInstallmentsError 17 | 18 | class Transaction(WebpayTransaction): 19 | CREATE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/' 20 | COMMIT_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 21 | STATUS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 22 | REFUND_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/refunds' 23 | CAPTURE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/capture' 24 | INSTALLMENTS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/installments' 25 | 26 | def __init__(self, options: WebpayOptions): 27 | super().__init__(options) 28 | 29 | def create(self, buy_order: str, session_id: str, amount: float, cvv: str, card_number: str, card_expiration_date: str): 30 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 31 | ValidationUtil.has_text_with_max_length(session_id, ApiConstants.SESSION_ID_LENGTH, "session_id") 32 | ValidationUtil.has_text_with_max_length(card_number, ApiConstants.CARD_NUMBER_LENGTH, "card_number") 33 | ValidationUtil.has_text_with_max_length(card_expiration_date, ApiConstants.CARD_EXPIRATION_DATE_LENGTH, "card_expiration_date") 34 | 35 | try: 36 | endpoint = Transaction.CREATE_ENDPOINT 37 | request = TransactionCreateRequest(buy_order, session_id, amount, card_number, cvv, card_expiration_date) 38 | return RequestService.post(endpoint, TransactionCreateRequestSchema().dumps(request), self.options) 39 | except TransbankError as e: 40 | raise TransactionCreateError(e.message, e.code) 41 | 42 | def commit(self, token: str, id_query_installments: str, deferred_period_index: int, grace_period: int): 43 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 44 | try: 45 | endpoint = Transaction.COMMIT_ENDPOINT.format(token) 46 | request = TransactionCommitRequest(id_query_installments, deferred_period_index, grace_period) 47 | return RequestService.put(endpoint, TransactionCommitRequestSchema().dumps(request), self.options) 48 | except TransbankError as e: 49 | raise TransactionCommitError(e.message, e.code) 50 | 51 | def status(self, token: str): 52 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 53 | try: 54 | endpoint = Transaction.STATUS_ENDPOINT.format(token) 55 | return RequestService.get(endpoint, self.options) 56 | except TransbankError as e: 57 | raise TransactionStatusError(e.message, e.code) 58 | 59 | def refund(self, token: str, amount: float): 60 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 61 | try: 62 | endpoint = Transaction.REFUND_ENDPOINT.format(token) 63 | request = TransactionRefundRequest(amount) 64 | return RequestService.post(endpoint, TransactionRefundRequestSchema().dumps(request), self.options) 65 | except TransbankError as e: 66 | raise TransactionRefundError(e.message, e.code) 67 | 68 | def capture(self, token: str, buy_order: str, authorization_code: str, capture_amount: float): 69 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 70 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 71 | ValidationUtil.has_text_with_max_length(authorization_code, ApiConstants.AUTHORIZATION_CODE_LENGTH, "authorization_code") 72 | try: 73 | endpoint = Transaction.CAPTURE_ENDPOINT.format(token) 74 | request = TransactionCaptureRequest(buy_order, authorization_code, capture_amount) 75 | return RequestService.put(endpoint, TransactionCaptureRequestSchema().dumps(request), self.options) 76 | except TransbankError as e: 77 | raise TransactionCaptureError(e.message, e.code) 78 | 79 | def installments(self, token: str, installments_number: int): 80 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 81 | try: 82 | endpoint = Transaction.INSTALLMENTS_ENDPOINT.format(token) 83 | request = TransactionInstallmentsRequest(installments_number) 84 | return RequestService.post(endpoint, TransactionInstallmentsRequestSchema().dumps(request), self.options) 85 | except TransbankError as e: 86 | raise TransactionInstallmentsError(e.message, e.code) 87 | 88 | -------------------------------------------------------------------------------- /tests/webpay/oneclick/test_mall_inscription.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import string 4 | import secrets 5 | from unittest.mock import Mock 6 | from transbank.webpay.oneclick.mall_inscription import * 7 | from tests.mocks.responses_api_mocks import responses 8 | from unittest.mock import patch 9 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 10 | from transbank.common.integration_api_keys import IntegrationApiKeys 11 | 12 | class OneclickMallInscriptionTestCase(unittest.TestCase): 13 | 14 | def setUp(self) -> None: 15 | self.username_mock = 'test_user' 16 | self.email_mock = 'test@user.test' 17 | self.return_url_mock = 'https://url_return.com' 18 | self.tbk_token_mock = '01ab5218921c3ffe06a19835b3fa7b4fcffa75965c14c7bda69ac7eeeb7d4245' 19 | self.tbk_user_mock = '08ed03b1-8fa6-4d7b-b35c-b134e1c5e9ee' 20 | self.mock_response = Mock() 21 | self.inscription = MallInscription.build_for_integration(IntegrationCommerceCodes.ONECLICK_MALL, IntegrationApiKeys.WEBPAY) 22 | 23 | @patch('transbank.common.request_service.requests.post') 24 | def test_inscription_start_transaction_successful(self, mock_post): 25 | self.mock_response.status_code = 200 26 | self.mock_response.text = json.dumps(responses['inscription_start_response']) 27 | mock_post.return_value = self.mock_response 28 | 29 | response = self.inscription.start(self.username_mock, self.email_mock, self.return_url_mock) 30 | 31 | self.assertEqual(response, responses['inscription_start_response']) 32 | 33 | def get_invalid_length_param(self) -> str: 34 | valid_string = string.ascii_letters + string.digits + "-._~" 35 | invalid_length_param = ''.join(secrets.choice(valid_string) for _ in range(ApiConstants.RETURN_URL_LENGTH + 1)) 36 | return invalid_length_param 37 | 38 | @patch('transbank.common.request_service.requests.post') 39 | def test_inscription_start_exception_not_authorized(self, mock_post): 40 | self.mock_response.status_code = 401 41 | self.mock_response.text = json.dumps(responses['create_error']) 42 | mock_post.return_value = self.mock_response 43 | 44 | with self.assertRaises(InscriptionStartError) as context: 45 | self.inscription.start(self.username_mock, self.email_mock, self.return_url_mock) 46 | 47 | self.assertTrue('Not Authorized' in context.exception.message) 48 | self.assertEqual(context.exception.__class__, InscriptionStartError) 49 | 50 | def test_inscription_start_exception_username_max_length(self): 51 | invalid_username = self.get_invalid_length_param() 52 | 53 | with self.assertRaises(TransbankError) as context: 54 | self.inscription.start(invalid_username, self.email_mock, self.return_url_mock) 55 | 56 | self.assertTrue("'username' is too long, the maximum length" in context.exception.message) 57 | self.assertEqual(context.exception.__class__, TransbankError) 58 | 59 | def test_inscription_start_exception_email_max_length(self): 60 | invalid_email = self.get_invalid_length_param() 61 | 62 | with self.assertRaises(TransbankError) as context: 63 | self.inscription.start(self.username_mock, invalid_email, self.return_url_mock) 64 | 65 | self.assertTrue("'email' is too long, the maximum length" in context.exception.message) 66 | self.assertEqual(context.exception.__class__, TransbankError) 67 | 68 | def test_inscription_start_exception_response_url_max_length(self): 69 | invalid_url = self.get_invalid_length_param() 70 | 71 | with self.assertRaises(TransbankError) as context: 72 | self.inscription.start(self.username_mock, self.email_mock, invalid_url) 73 | 74 | print(context.exception.message) 75 | self.assertTrue("'response_url' is too long, the maximum length" in context.exception.message) 76 | self.assertEqual(context.exception.__class__, TransbankError) 77 | 78 | @patch('transbank.common.request_service.requests.put') 79 | def test_inscription_finish_transaction_successful(self, mock_put): 80 | self.mock_response.status_code = 200 81 | self.mock_response.text = json.dumps(responses['inscription_finish_response']) 82 | mock_put.return_value = self.mock_response 83 | 84 | response = self.inscription.finish(self.tbk_token_mock) 85 | 86 | self.assertEqual(response, responses['inscription_finish_response']) 87 | 88 | @patch('transbank.common.request_service.requests.put') 89 | def test_inscription_finish_transaction_fail(self, mock_put): 90 | self.mock_response.status_code = 200 91 | self.mock_response.text = json.dumps(responses['inscription_finish_fail']) 92 | mock_put.return_value = self.mock_response 93 | 94 | response = self.inscription.finish(self.tbk_token_mock) 95 | 96 | self.assertEqual(response, responses['inscription_finish_fail']) 97 | 98 | @patch('transbank.common.request_service.requests.put') 99 | def test_inscription_finish_exception(self, mock_put): 100 | self.mock_response.status_code = 500 101 | self.mock_response.text = json.dumps(responses['general_error']) 102 | mock_put.return_value = self.mock_response 103 | 104 | with self.assertRaises(InscriptionFinishError) as context: 105 | self.inscription.finish(self.tbk_token_mock) 106 | 107 | self.assertEqual(context.exception.__class__, InscriptionFinishError) 108 | 109 | def test_inscription_delete_exception_empty_tbk_user(self): 110 | empty_tbk_user = '' 111 | 112 | with self.assertRaises(TransbankError) as context: 113 | self.inscription.delete(empty_tbk_user, self.username_mock) 114 | 115 | self.assertTrue("'tbk_user' can't be null or white space" in context.exception.message) 116 | self.assertEqual(context.exception.__class__, TransbankError) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Publish Status](https://github.com/TransbankDevelopers/transbank-sdk-python/actions/workflows/publish.yml/badge.svg) 2 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=TransbankDevelopers_transbank-sdk-python&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=TransbankDevelopers_transbank-sdk-python) 3 | [![PyPI version](https://badge.fury.io/py/transbank-sdk.svg)](https://badge.fury.io/py/transbank-sdk) 4 | 5 | # Transbank Python SDK 6 | 7 | SDK Oficial de Transbank 8 | 9 | ## Requisitos: 10 | 11 | - Python 3.12+ 12 | - [Pipenv](https://github.com/pypa/pipenv) 13 | - Plugin de editorconfig para tu editor favorito. 14 | 15 | # Instalación 16 | 17 | Puedes instalar el SDK directamente utilizando pip mediante el comando: 18 | 19 | ```bash 20 | pip install transbank-sdk 21 | ``` 22 | 23 | O puedes instalar el SDK a través de Pipenv, agregando a Pipfile: 24 | 25 | ```python 26 | [packages] 27 | transbank-sdk = '*' 28 | ``` 29 | 30 | y luego ejecutar: 31 | 32 | ```bash 33 | pipenv install 34 | ``` 35 | 36 | ### Test 37 | 38 | Para ejecutar los test localmente debes usar los siguientes comandos en una terminal. 39 | 40 | ```bash 41 | pipenv install 42 | pipenv install --dev 43 | pipenv run tests 44 | ``` 45 | 46 | ## Documentación 47 | 48 | Puedes encontrar toda la documentación de cómo usar este SDK en el sitio https://www.transbankdevelopers.cl. 49 | 50 | La documentación relevante para usar este SDK es: 51 | 52 | - Documentación general sobre los productos y sus diferencias: 53 | [Webpay](https://www.transbankdevelopers.cl/producto/webpay). 54 | - Documentación sobre [ambientes, deberes del comercio, puesta en producción, 55 | etc](https://www.transbankdevelopers.cl/documentacion/como_empezar#ambientes). 56 | - Primeros pasos con [Webpay](https://www.transbankdevelopers.cl/documentacion/webpay). 57 | - Referencia detallada sobre [Webpay](https://www.transbankdevelopers.cl/referencia/webpay). 58 | 59 | ## Información para contribuir a este proyecto 60 | 61 | ### Forma de trabajo 62 | 63 | - Para los mensajes de commits, nos basamos en las [Git Commit Guidelines de Angular](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits). 64 | - Usamos inglés para los nombres de ramas y mensajes de commit. 65 | - Los mensajes de commit no deben llevar punto final. 66 | - Los mensajes de commit deben usar un lenguaje imperativo y estar en tiempo presente, por ejemplo, usar "change" en lugar de "changed" o "changes". 67 | - Los nombres de las ramas deben estar en minúsculas y las palabras deben separarse con guiones (-). 68 | - Todas las fusiones a la rama principal se deben realizar mediante solicitudes de Pull Request(PR). ⬇️ 69 | - Se debe emplear tokens como "WIP" en el encabezado de un commit, separados por dos puntos (:), por ejemplo, "WIP: this is a useful commit message". 70 | - Una rama con nuevas funcionalidades que no tenga un PR, se considera que está en desarrollo. 71 | - Los nombres de las ramas deben comenzar con uno de los tokens definidos. Por ejemplo: "feat/tokens-configurations". 72 | 73 | ### Short lead tokens permitidos 74 | 75 | `WIP` = En progreso. 76 | 77 | `feat` = Nuevos features. 78 | 79 | `fix` = Corrección de un bug. 80 | 81 | `docs` = Cambios solo de documentación. 82 | 83 | `style` = Cambios que no afectan el significado del código. (espaciado, formateo de código, comillas faltantes, etc) 84 | 85 | `refactor` = Un cambio en el código que no arregla un bug ni agrega una funcionalidad. 86 | 87 | `perf` = Cambio que mejora el rendimiento. 88 | 89 | `test` = Agregar test faltantes o los corrige. 90 | 91 | `chore` = Cambios en el build o herramientas auxiliares y librerías. 92 | 93 | `revert` = Revierte un commit. 94 | 95 | `release` = Para liberar una nueva versión. 96 | 97 | ### Creación de un Pull Request 98 | 99 | - El PR debe estar enfocado en un cambio en concreto, por ejemplo, agregar una nueva funcionalidad o solucionar un error, pero un solo PR no puede agregar una nueva funcionalidad y arreglar un error. 100 | - El título del los PR y mensajes de commit no debe comenzar con una letra mayúscula. 101 | - No se debe usar punto final en los títulos. 102 | - El título del PR debe comenzar con el short lead token definido para la rama, seguido de ":"" y una breve descripción del cambio. 103 | - La descripción del PR debe detallar los cambios que se están incorporando. 104 | - La descripción del PR debe incluir evidencias de que los test se ejecutan de forma correcta o incluir evidencias de que los cambios funcionan y no afectan la funcionalidad previa del proyecto. 105 | - Se pueden agregar capturas, gif o videos para complementar la descripción o demostrar el funcionamiento del PR. 106 | 107 | #### Flujo de trabajo 108 | 109 | 1. Crea tu rama desde develop. 110 | 2. Haz un push de los commits y publica la nueva rama. 111 | 3. Abre un Pull Request apuntando tus cambios a develop. 112 | 4. Espera a la revisión de los demás integrantes del equipo. 113 | 5. Para poder mezclar los cambios se debe contar con 2 aprobaciones de los revisores y no tener alertas por parte de las herramientas de inspección. 114 | 115 | ### Esquema de flujo con git 116 | 117 | ![gitflow](https://wac-cdn.atlassian.com/dam/jcr:cc0b526e-adb7-4d45-874e-9bcea9898b4a/04%20Hotfix%20branches.svg?cdnVersion=1324) 118 | 119 | ## Generar una nueva versión 120 | 121 | Para generar una nueva versión, se debe crear un PR (con un título "Prepare release X.Y.Z" con los valores que correspondan para `X`, `Y` y `Z`). Se debe seguir el estándar semver para determinar si se incrementa el valor de `X` (si hay cambios no retrocompatibles), `Y` (para mejoras retrocompatibles) o `Z` (si sólo hubo correcciones a bugs). 122 | 123 | En ese PR deben incluirse los siguientes cambios: 124 | 125 | 1. Modificar el archivo `CHANGELOG.md` para incluir una nueva entrada (al comienzo) para `X.Y.Z` que explique en español los cambios **de cara al usuario del SDK**. 126 | 2. Modificar [**version.py**](./transbank/__version__.py) para que apunte a la nueva versión `X.Y.Z`. 127 | 128 | Luego de obtener aprobación del pull request, debe mezclarse a master e inmediatamente generar un release en GitHub con el tag `vX.Y.Z`. En la descripción del release debes poner lo mismo que agregaste al changelog. 129 | 130 | Con eso Travis CI generará automáticamente una nueva versión de la librería y la publicará en PyPI. 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Todos los cambios notables a este proyecto serán documentados en este archivo. 4 | 5 | El formato está basado en [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | y este proyecto adhiere a [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [6.1.0] - 2025-06-24 9 | 10 | Esta versión agrega una clase para la nueva funcionalidad de la API de OneClick. Los métodos existentes no tienen cambios. 11 | 12 | ### Agrega: 13 | 14 | - Se agrega la clase MallBinInfo , la cual contiene el método query_bin para la consulta de información de una tarjeta registrada en OneClick. 15 | 16 | ### Actualiza: 17 | 18 | - Se actualizan las dependencias necesarias para construir el proyecto 19 | 20 | ## [6.0.0] - 2025-05-05 21 | 22 | Esta versión no tiene cambios en el comportamiento de las funcionalidades de la API. 23 | 24 | ¡Importante! 25 | El SDK ya no apunta por defecto al ambiente de integración. Ahora es necesario configurar de forma explícita las credenciales. Para esto se debe inicializar explícitamente los objetos de los distintos productos, ya sea utilizando la clase Options o a través de los nuevos métodos build_for_integration y build_for_production. 26 | 27 | ### Agrega 28 | 29 | - Se agrega el parámetro timeout para las peticiones a la API para que pueda modificarse en todos los productos. 30 | - Se agregan los métodos build_for_integration y build_for_production a todos los productos. 31 | 32 | ### Actualiza 33 | 34 | - Se configura por defecto el timeout a 600 segundos para todas las peticiones. 35 | - Se actualizan las versiones de las dependencias. 36 | - Se actualizan los test. 37 | 38 | ### Elimina 39 | 40 | - Se elimina el código que hace referencia al producto Webpay Modal. 41 | - Se elimina el código que hace referencia al producto PatPass by Webpay. 42 | - Se eliminan los métodos configure_for_integration, configure_for_production, configure_for_testing, configure_for_testing_deferred, configure_for_testing_sin_cvv, configure_for_testing_deferred_sin_cvv de todos los productos que los utilizaban. 43 | 44 | ## [5.0.0] - 2024-02-28 45 | 46 | ### Changed 47 | 48 | - Se hace downgrade al API de la versión 1.3 a la versión 1.2. 49 | 50 | ### Fixed 51 | 52 | - Retorna un boolean en el método delete para la Inscripción de Oneclick Mall. 53 | - Se corrige error en el método 'refund' de Transacción Completa. 54 | 55 | ## [4.0.0] - 2022-09-20 56 | 57 | ### Changed 58 | 59 | - Se migra el API desde la versión 1.2 a la versión 1.3 60 | 61 | ### Added 62 | 63 | - Se agrega los métodos 'increaseAmount', 'increaseAuthorizationDate', 'reversePreAuthorizedAmount' y 'deferredCaptureHistory' a las versiones diferidas de WebpayPlus, WebpayPlus Mall, Oneclick Mall, Transacción Completa y Transacción Completa Mall 64 | - Ahora los métodos status y commit de las versiones diferidas de WebpayPlus, WebpayPlus Mall, Transacción Completa y Transacción Completa Mall retornan el campo 'captureExpirationDate'. Para Oneclick Mall este campo también se agrega en los detalles de la autorización 65 | 66 | ## [3.0.1] - 2022-07-13 67 | 68 | ### Fixed 69 | 70 | - Actualización de versión mínima requerida de dependencia Marshmallow. 71 | - Se corrige el método 'has_text' de la clase 'ValidationUtil'. [PR #97](https://github.com/TransbankDevelopers/transbank-sdk-python/pull/97) de [@aduquehd](https://github.com/aduquehd) 72 | 73 | ## [3.0.0] - 2022-01-27 74 | 75 | ### Removed 76 | 77 | - Se elimina Onepay 78 | 79 | ### Changed 80 | 81 | - Se refactoriza y migra todos los productos desde clases estáticas a clases instanciables 82 | - Todas las respuestas de los métodos pasan a ser 'dictionaries' 83 | - Se unifica 'Transaction' y 'DeferredTransaction' en WebpayPlus 84 | - Se unifica 'MallTransaction' y 'MallDeferredTransaction' en WebpayPlus y Oneclick 85 | - Se reordenan los parámetros del método refund de WebpayPlus Mall a 'refund(token: str, child_buy_order: str, child_commerce_code:str, amount: float)' 86 | - Se reordenan los parámetros del método capture de WebpayPlus Mall a 'capture(child_commerce_code: str, token: str, buy_order: str, authorization_code: str, capture_amount: float)' 87 | - Se reordenan los parámetros del método create de Transacción Completa a 'create(buy_order: str, session_id: str, amount: float, cvv: str, card_number: str, card_expiration_date: str) 88 | - Se reordenan los parámetros del método create de Transacción Completa Mall a 'create(buy_order: str, session_id: str, card_number: str, card_expiration_date: str, details: list, cvv: str = None)' 89 | 90 | ### Added 91 | 92 | - Se agrega soporte a Webpay Modal 93 | - Se agregan validaciones de obligatoriedad y tamaño de los parámetros a los métodos de WebpayPlus, Oneclick, Webpay Modal, Transacción Completa 94 | - Se agrega una clase de constantes con los códigos de comercio de integración: 'IntegrationCommerceCodes' 95 | - Se agrega una clase de constantes con las claves de comercio de integración: 'IntegrationApiKeys' 96 | - Se agrega el método capture a Oneclick 'capture(child_commerce_code: str, child_buy_order: str, authorization_code: str, capture_amount: float)' 97 | 98 | ## [2.0.1] - 2021-10-28 99 | 100 | ### Fixed 101 | 102 | - Actualización de versión mínima requerida de dependencia Marshmallow. 103 | 104 | ### Security 105 | 106 | - Actualización de dependencia urllib3 a una versión libre de vulnerabilidades. 107 | 108 | ## [2.0.0] - 2021-10-19 109 | 110 | ### Added 111 | 112 | Los métodos apuntan a la versión 1.2 del API de Transbank, por lo que ahora las redirecciones de vuelta en el 113 | returnUrl serán por GET en vez de POST. 114 | 115 | ## [1.5.0] - 2021-05-27 116 | 117 | ### Added 118 | 119 | - Se agrega soporte para Captura Diferida en Transacción Completa modalidad normal y mall. 120 | 121 | ## [1.4.0] - 2021-02-25 122 | 123 | ### Added 124 | 125 | - Se agregan métodos para hacer más simple la configuración de Webpay Plus 126 | - Se agregan tests en Webpay Plus 127 | 128 | ### Fixed 129 | 130 | - Se arregla acumulación en transacciones mall. Gracias @jalvaradosegura 131 | - Se arreglan llamadas a estado en transacción inicializada 132 | - Se arregla llamada a commit en pagos usando Onepay dentro de Webpay 133 | 134 | ## [1.3.0] - 2020-11-12 135 | 136 | ### Added 137 | 138 | - Se agrega soporte para: 139 | - Webpay Plus Rest 140 | - modalidad normal 141 | - modalidad captura diferida 142 | - modalidad mall 143 | - modalidad mall captura diferida 144 | - Patpass by Webpay Rest 145 | - Patpass Comercio Rest 146 | - Transacción completa Rest 147 | - modalidad mall 148 | 149 | ### Fixed 150 | 151 | - Se arregla constructor de Oneclick Inscription Finish para soportar parámetros opcionales al abortar pago. Gracias a @atpollmann 152 | 153 | ## [1.2.1] - 2020-10-08 154 | 155 | ### Fixed 156 | 157 | - Se arregla error en la respuesta de OneClick Mall [PR #69](https://github.com/TransbankDevelopers/transbank-sdk-python/pull/69) de [@hsandovaltides](https://github.com/hsandovaltides) 158 | - Ahora se lanza excepción si se pasa un valor que no sea integer en el campo amount. [PR 68](ttps://github.com/TransbankDevelopers/transbank-sdk-python/pull/68) 159 | 160 | ## [1.2.0] - 2019-12-26 161 | 162 | ### Added 163 | 164 | - Se agrega soporte para Oneclick Mall y Transacción Completa en sus versiones REST. 165 | 166 | ## [1.1.0] - 2019-04-04 167 | 168 | ### Added 169 | 170 | - Se agregaron los parámetros `qr_width_height` y `commerce_logo_url` a Options, para especificar el tamaño del QR generado para la transacción, y especificar la ubicación del logo de comercio para ser mostrado en la aplicación móvil de Onepay. Puedes configurar estos parámetros globalmente o por transacción. 171 | 172 | ## [1.0.1] - 2018-11-07 173 | 174 | ### Fixed 175 | 176 | - En Onepay, se corrige error que impedía crear una transacción desde iOS. 177 | 178 | ### Security 179 | 180 | - Actualización de dependencia a una versión libre de vulnerabilidades. 181 | 182 | ## [1.0.0] - 2018-10-23 183 | 184 | ### Added 185 | 186 | - Primera versión del SDK de Transbank, que contiene solamente las funcionalidades para implementar Onepay. 187 | -------------------------------------------------------------------------------- /transbank/webpay/transaccion_completa/mall_transaction.py: -------------------------------------------------------------------------------- 1 | from transbank.common.options import WebpayOptions 2 | from transbank.common.request_service import RequestService 3 | from transbank.common.api_constants import ApiConstants 4 | from transbank.common.webpay_transaction import WebpayTransaction 5 | from transbank.common.validation_util import ValidationUtil 6 | from transbank.webpay.transaccion_completa.mall_request import TransactionCreateRequest, TransactionCommitRequest, \ 7 | TransactionRefundRequest, TransactionCaptureRequest, TransactionInstallmentsRequest 8 | from transbank.webpay.transaccion_completa.mall_schema import TransactionCreateRequestSchema, \ 9 | TransactionCommitRequestSchema, TransactionInstallmentsRequestSchema, TransactionRefundRequestSchema, TransactionCaptureRequestSchema 10 | from transbank.error.transbank_error import TransbankError 11 | from transbank.error.transaction_create_error import TransactionCreateError 12 | from transbank.error.transaction_commit_error import TransactionCommitError 13 | from transbank.error.transaction_status_error import TransactionStatusError 14 | from transbank.error.transaction_refund_error import TransactionRefundError 15 | from transbank.error.transaction_capture_error import TransactionCaptureError 16 | from transbank.error.transaction_installments_error import TransactionInstallmentsError 17 | 18 | class MallTransaction(WebpayTransaction): 19 | CREATE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/' 20 | COMMIT_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 21 | STATUS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}' 22 | REFUND_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/refunds' 23 | CAPTURE_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/capture' 24 | INSTALLMENTS_ENDPOINT = ApiConstants.WEBPAY_ENDPOINT + '/transactions/{}/installments' 25 | 26 | def __init__(self, options: WebpayOptions): 27 | super().__init__(options) 28 | 29 | def create(self, buy_order: str, session_id: str, card_number: str, card_expiration_date: str, details: list, cvv: str = None): 30 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 31 | ValidationUtil.has_text_with_max_length(session_id, ApiConstants.SESSION_ID_LENGTH, "session_id") 32 | ValidationUtil.has_text_with_max_length(card_number, ApiConstants.CARD_NUMBER_LENGTH, "card_number") 33 | ValidationUtil.has_text_with_max_length(card_expiration_date, ApiConstants.CARD_EXPIRATION_DATE_LENGTH, "card_expiration_date") 34 | ValidationUtil.has_elements(details, "details") 35 | for item in details: 36 | ValidationUtil.has_text_with_max_length(item['commerce_code'], ApiConstants.COMMERCE_CODE_LENGTH, "details.commerce_code") 37 | ValidationUtil.has_text_with_max_length(item['buy_order'], ApiConstants.BUY_ORDER_LENGTH, "details.buy_order") 38 | try: 39 | endpoint = MallTransaction.CREATE_ENDPOINT 40 | request = TransactionCreateRequest(buy_order, session_id, card_number, card_expiration_date, details, cvv) 41 | return RequestService.post(endpoint, TransactionCreateRequestSchema().dumps(request), self.options) 42 | except TransbankError as e: 43 | raise TransactionCreateError(e.message, e.code) 44 | 45 | def commit(self, token: str, details: list): 46 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 47 | try: 48 | endpoint = MallTransaction.COMMIT_ENDPOINT.format(token) 49 | request = TransactionCommitRequest(details) 50 | return RequestService.put(endpoint, TransactionCommitRequestSchema().dumps(request), self.options) 51 | except TransbankError as e: 52 | raise TransactionCommitError(e.message, e.code) 53 | 54 | def status(self, token: str): 55 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 56 | try: 57 | endpoint = MallTransaction.STATUS_ENDPOINT.format(token) 58 | return RequestService.get(endpoint, self.options) 59 | except TransbankError as e: 60 | raise TransactionStatusError(e.message, e.code) 61 | 62 | def refund(self, token: str, child_buy_order: str, child_commerce_code: str, amount: str): 63 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 64 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 65 | ValidationUtil.has_text_with_max_length(child_buy_order, ApiConstants.BUY_ORDER_LENGTH, "child_buy_order") 66 | try: 67 | endpoint = MallTransaction.REFUND_ENDPOINT.format(token) 68 | request = TransactionRefundRequest(buy_order=child_buy_order, commerce_code=child_commerce_code, amount=amount) 69 | return RequestService.post(endpoint, TransactionRefundRequestSchema().dumps(request), self.options) 70 | except TransbankError as e: 71 | raise TransactionRefundError(e.message, e.code) 72 | 73 | def capture(self, token: str, child_commerce_code: str, child_buy_order: str, authorization_code: str, capture_amount: float): 74 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 75 | ValidationUtil.has_text_with_max_length(child_commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "child_commerce_code") 76 | ValidationUtil.has_text_with_max_length(child_buy_order, ApiConstants.BUY_ORDER_LENGTH, "child_buy_order") 77 | ValidationUtil.has_text_with_max_length(authorization_code, ApiConstants.AUTHORIZATION_CODE_LENGTH, "authorization_code") 78 | try: 79 | endpoint = MallTransaction.CAPTURE_ENDPOINT.format(token) 80 | request = TransactionCaptureRequest(commerce_code=child_commerce_code, buy_order=child_buy_order, 81 | authorization_code=authorization_code, capture_amount=capture_amount) 82 | return RequestService.put(endpoint, TransactionCaptureRequestSchema().dumps(request), self.options) 83 | except TransbankError as e: 84 | raise TransactionCaptureError(e.message, e.code) 85 | 86 | def installments(self, token: str, details: list): 87 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 88 | ValidationUtil.has_elements(details, "details") 89 | for item in details: 90 | ValidationUtil.has_text_with_max_length(item['commerce_code'], ApiConstants.COMMERCE_CODE_LENGTH, "details.commerce_code") 91 | ValidationUtil.has_text_with_max_length(item['buy_order'], ApiConstants.BUY_ORDER_LENGTH, "details.buy_order") 92 | try: 93 | return [ 94 | self.single_installment(token, 95 | installments_number=det['installments_number'], 96 | buy_order=det['buy_order'], 97 | commerce_code=det['commerce_code'] 98 | ) for det in details 99 | ] 100 | except TransbankError as e: 101 | raise TransactionInstallmentsError(e.message, e.code) 102 | 103 | def single_installment(self, token: str, installments_number: float, buy_order: str, commerce_code: str): 104 | ValidationUtil.has_text_with_max_length(token, ApiConstants.TOKEN_LENGTH, "token") 105 | ValidationUtil.has_text_with_max_length(commerce_code, ApiConstants.COMMERCE_CODE_LENGTH, "commerce_code") 106 | ValidationUtil.has_text_with_max_length(buy_order, ApiConstants.BUY_ORDER_LENGTH, "buy_order") 107 | try: 108 | endpoint = MallTransaction.INSTALLMENTS_ENDPOINT.format(token) 109 | request = TransactionInstallmentsRequest(installments_number=installments_number, buy_order=buy_order, 110 | commerce_code=commerce_code) 111 | return RequestService.post(endpoint, TransactionInstallmentsRequestSchema().dumps(request), self.options) 112 | except TransbankError as e: 113 | raise TransactionInstallmentsError(e.message, e.code) 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/mocks/responses_api_mocks.py: -------------------------------------------------------------------------------- 1 | responses = { 2 | 'create_response': { 3 | 'token': '01ab69087c923abf08331e7bc42b4af10140f2d3f2e54e53d7ae01aebe6ddc52', 4 | 'url': 'https://webpay3gint.transbank.cl/webpayserver/initTransaction' 5 | }, 6 | 'create_error': { 7 | 'error_message': 'Not Authorized' 8 | }, 9 | 'commit_status_response': { 10 | 'vci': 'TSY', 11 | 'amount': 150000, 12 | 'status': 'AUTHORIZED', 13 | 'buy_order': 'buy_order_mock_123456789', 14 | 'session_id': 'session_ide_mock_123456789', 15 | 'card_detail': { 16 | 'card_number': '6623' 17 | }, 18 | 'accounting_date': '0624', 19 | 'transaction_date': '2020-06-24T12:26:21.463Z', 20 | 'authorization_code': '1213', 21 | 'payment_type_code': 'VN', 22 | 'response_code': 0, 23 | 'installments_number': 0 24 | }, 25 | 'reversed_response': { 26 | 'type': 'REVERSED' 27 | }, 28 | 'nullified_response': { 29 | 'type': 'NULLIFIED', 30 | 'balance': 16886, 31 | 'authorization_code': '594213', 32 | 'response_code': 0, 33 | 'authorization_date': '2023-10-01T04:16:06.565Z', 34 | 'nullified_amount': 300000 35 | }, 36 | 'error_api_mismatch': { 37 | 'error_message': 'Api mismatch error, required version is 1.3' 38 | }, 39 | 'commit_error': { 40 | 'error_message': 'Invalid status 0 for transaction while authorizing. Commerce will be notified by webpay to' 41 | ' authorize' 42 | }, 43 | 'general_error': { 44 | 'description': 'Internal server error' 45 | }, 46 | 'expired_token': { 47 | 'error_message': 'The transactions date has passed max time (7 days) to recover the status' 48 | }, 49 | 'invalid_parameter': { 50 | 'error_message': 'Invalid value for parameter: amount' 51 | }, 52 | 'required_parameter': { 53 | 'error_message': 'amount is required!' 54 | }, 55 | 'commit_deferred': { 56 | 'vci': 'TSY', 57 | 'amount': 1209, 58 | 'status': 'AUTHORIZED', 59 | 'buy_order': 'O-74351', 60 | 'session_id': 'S-72021', 61 | 'card_detail': { 62 | 'card_number': '6623' 63 | }, 64 | 'accounting_date': '1004', 65 | 'transaction_date': '2023-10-04T12:48:34.770Z', 66 | 'authorization_code': '123456', 67 | 'payment_type_code': 'VN', 68 | 'response_code': 0, 69 | 'installments_number': 0, 70 | 'capture_expiration_date': '2023-11-03T12:49:26.709Z' 71 | }, 72 | 'increase_amount_response': { 73 | 'authorization_code': '123456', 74 | 'authorization_date': '2023-10-04T12:51:36Z', 75 | 'total_amount': 2209, 76 | 'expiration_date': '2023-11-03T12:49:26.709Z', 77 | 'response_code': 0 78 | }, 79 | 'increase_date_response': 80 | { 81 | 'authorization_code': '123456', 82 | 'authorization_date': '2023-10-04T12:52:51Z', 83 | 'total_amount': 2209, 84 | 'expiration_date': '2023-11-03T12:52:51.108Z', 85 | 'response_code': 0 86 | }, 87 | 'capture_history_response': [ 88 | { 89 | 'type': 'Preauthorization', 90 | 'amount': 1209, 91 | 'authorization_code': '123456', 92 | 'authorization_date': '2023-10-04T12:49:26.709Z', 93 | 'total_amount': 1209, 94 | 'expiration_date': '2023-11-03T12:49:26.709Z', 95 | 'response_code': 0 96 | }, 97 | { 98 | 'type': 'Amount adjustment', 99 | 'amount': 1000, 100 | 'authorization_code': '123456', 101 | 'authorization_date': '2023-10-04T12:51:36.577Z', 102 | 'total_amount': 2209, 103 | 'expiration_date': '2023-11-03T12:49:26.709Z', 104 | 'response_code': 0 105 | }, 106 | { 107 | 'type': 'Expiration date adjustment', 108 | 'amount': 0, 109 | 'authorization_code': '123456', 110 | 'authorization_date': '2023-10-04T12:52:51.108Z', 111 | 'total_amount': 2209, 112 | 'expiration_date': '2023-11-03T12:52:51.108Z', 113 | 'response_code': 0 114 | } 115 | ], 116 | 'capture_response': { 117 | 'authorization_code': '123456', 118 | 'authorization_date': '2023-10-04T12:55:49Z', 119 | 'captured_amount': 2209, 120 | 'response_code': 0 121 | }, 122 | 'reverse_preauthorized_amount': { 123 | 'authorization_code': '123456', 124 | 'authorization_date': '2023-10-04T13:01:04Z', 125 | 'total_amount': 1126, 126 | 'expiration_date': '2023-11-03T13:00:44.751Z', 127 | 'response_code': 0 128 | }, 129 | 'invalid_parameter_capture': { 130 | 'error_message': 'Invalid value for parameter: capture_amount' 131 | }, 132 | 'transaction_detail_not_found': { 133 | 'error_message': 'Invalid value for parameter: Transaction Detail not found' 134 | }, 135 | 'transaction_not_found': { 136 | 'error_message': 'Transaction not found' 137 | }, 138 | 'commit_mall': { 139 | 'vci': 'TSY', 140 | 'details': [{ 141 | 'amount': 1000, 142 | 'status': 'AUTHORIZED', 143 | 'authorization_code': '1213', 144 | 'payment_type_code': 'VN', 145 | 'response_code': 0, 146 | 'installments_number': 0, 147 | 'commerce_code': '597055555536', 148 | 'buy_order': 'child_buy_order1_mock_123' 149 | }, { 150 | 'amount': 2000, 151 | 'status': 'AUTHORIZED', 152 | 'authorization_code': '1213', 153 | 'payment_type_code': 'VN', 154 | 'response_code': 0, 155 | 'installments_number': 0, 156 | 'commerce_code': '597055555537', 157 | 'buy_order': 'child_buy_order2_mock_123' 158 | }], 159 | 'buy_order': 'mall_buy_order_mock_123', 160 | 'session_id': 'session_id_mock_123456789', 161 | 'card_detail': {'card_number': '6623'}, 162 | 'accounting_date': '1011', 163 | 'transaction_date': '2023-10-15T21:10:29.395Z' 164 | }, 165 | 'bigger_amount_mall': { 166 | 'error_message': 'Amount to refund is bigger than authorized' 167 | }, 168 | 'status_mall_deferred': { 169 | 'vci': 'TSY', 170 | 'details': [{ 171 | 'amount': 1000, 172 | 'status': 'AUTHORIZED', 173 | 'authorization_code': '123456', 174 | 'payment_type_code': 'VN', 175 | 'response_code': 0, 176 | 'installments_number': 0, 177 | 'commerce_code': '597055555582', 178 | 'buy_order': 'abcdef55', 179 | 'capture_expiration_date': '2023-11-15T23:20:55.499Z' 180 | }, { 181 | 'amount': 2000, 182 | 'status': 'AUTHORIZED', 183 | 'authorization_code': '123456', 184 | 'payment_type_code': 'VN', 185 | 'response_code': 0, 186 | 'installments_number': 0, 187 | 'commerce_code': '597055555583', 188 | 'buy_order': 'wxyz55', 189 | 'capture_expiration_date': '2023-11-15T23:20:55.672Z' 190 | }], 191 | 'buy_order': 'buyorder55', 192 | 'session_id': 'session55', 193 | 'card_detail': {'card_number': '6623'}, 194 | 'accounting_date': '1016', 195 | 'transaction_date': '2023-10-16T23:20:11.653Z' 196 | }, 197 | 'inscription_start_response': { 198 | 'token': '01ab844d8fa41f98b4ccfeef3d254235eadc9a5a39cb86498d807e10e5b00f9b', 199 | 'url_webpay': 'https://webpay3gint.transbank.cl/webpayserver/bp_multicode_inscription.cgi' 200 | }, 201 | 'inscription_finish_response': { 202 | 'response_code': 0, 203 | 'tbk_user': '08ed03b1-8fa6-4d7b-b35c-b134e1c5e9ee', 204 | 'authorization_code': '1213', 205 | 'card_type': 'Visa', 206 | 'card_number': 'XXXXXXXXXXXX6623' 207 | }, 208 | 'inscription_finish_fail': { 209 | 'response_code': -1 210 | }, 211 | 'authorize_response': { 212 | 'details': [ 213 | { 214 | 'amount': 1693, 215 | 'status': 'AUTHORIZED', 216 | 'authorization_code': '1213', 217 | 'payment_type_code': 'VN', 218 | 'response_code': 0, 219 | 'installments_number': 0, 220 | 'commerce_code': '597055555542', 221 | 'buy_order': 'child_buy_order_1' 222 | } 223 | ], 224 | 'buy_order': 'parent_buy_order', 225 | 'card_detail': { 226 | 'card_number': '6623' 227 | }, 228 | 'accounting_date': '1019', 229 | 'transaction_date': '2023-10-19T21:30:21.095Z' 230 | }, 231 | 'deferred_authorize_response': { 232 | 'details': [ 233 | { 234 | 'amount': 2000, 235 | 'status': 'AUTHORIZED', 236 | 'authorization_code': '123456', 237 | 'payment_type_code': 'VN', 238 | 'response_code': 0, 239 | 'installments_number': 0, 240 | 'commerce_code': '597055555548', 241 | 'buy_order': 'child_buy_order_2', 242 | 'capture_expiration_date': '2023-11-19T11:52:39.753Z' 243 | } 244 | ], 245 | 'buy_order': 'parent_buy_order', 246 | 'card_detail': { 247 | 'card_number': '6623' 248 | }, 249 | 'accounting_date': '1020', 250 | 'transaction_date': '2023-10-20T11:52:39.571Z' 251 | }, 252 | 'captured_status_response': { 253 | 'details': [ 254 | { 255 | 'amount': 2000, 256 | 'status': 'CAPTURED', 257 | 'authorization_code': '123456', 258 | 'payment_type_code': 'VN', 259 | 'response_code': 0, 260 | 'installments_number': 0, 261 | 'commerce_code': '597055555548', 262 | 'buy_order': 'child_buy_order_2', 263 | 'capture_expiration_date': '2023-11-19T15:44:13.429Z' 264 | } 265 | ], 266 | 'buy_order': 'parent_buy_order', 267 | 'card_detail': { 268 | 'card_number': '6623' 269 | }, 270 | 'accounting_date': '1020', 271 | 'transaction_date': '2023-10-20T15:44:13.111Z' 272 | }, 273 | 'buy_order_not_found': { 274 | 'error_message': 'Invalid value for parameter: buy order not found' 275 | }, 276 | 'already_refunded_error': { 277 | 'error_message': 'Transaction already fully refunded' 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /tests/webpay/plus/test_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | from unittest.mock import patch 4 | import json 5 | import secrets 6 | import string 7 | from transbank.webpay.webpay_plus.transaction import * 8 | from transbank.error.transaction_create_error import TransactionCreateError 9 | from tests.mocks.responses_api_mocks import responses 10 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 11 | from transbank.common.integration_api_keys import IntegrationApiKeys 12 | 13 | 14 | class TransactionTestCase(unittest.TestCase): 15 | 16 | def setUp(self) -> None: 17 | self.buy_order_mock = 'buy_order_mock_123456789' 18 | self.session_id_mock = 'session_ide_mock_123456789' 19 | self.amount_mock = 150000 20 | self.return_url_mock = "https://url_return.com" 21 | self.token_mock = '01abf2be20aad1da804aeae1ed3062fb8fba108ee0e07f4d37181f51c3f6714d' 22 | self.invalid_amount = -1000 23 | self.authorization_code_mock = '123456' 24 | self.capture_amount_mock = 150000 25 | self.mock_response = Mock() 26 | self.transaction = Transaction.build_for_integration( 27 | IntegrationCommerceCodes.WEBPAY_PLUS, IntegrationApiKeys.WEBPAY) 28 | 29 | def test_create_transaction_successful_to_api(self): 30 | response = self.transaction.create(self.buy_order_mock, self.session_id_mock, self.amount_mock, 31 | self.return_url_mock) 32 | 33 | self.assertEqual( 34 | response['url'], 'https://webpay3gint.transbank.cl/webpayserver/initTransaction') 35 | 36 | @patch('transbank.common.request_service.requests.post') 37 | def test_create_transaction_successful(self, mock_post): 38 | self.mock_response.status_code = 200 39 | self.mock_response.text = json.dumps(responses['create_response']) 40 | mock_post.return_value = self.mock_response 41 | 42 | response = self.transaction.create(self.buy_order_mock, self.session_id_mock, self.amount_mock, 43 | self.return_url_mock) 44 | 45 | self.assertEqual(response, responses['create_response']) 46 | 47 | @patch('transbank.common.request_service.requests.post') 48 | def test_create_exception_not_authorized(self, mock_post): 49 | self.mock_response.status_code = 401 50 | self.mock_response.text = json.dumps(responses['create_error']) 51 | mock_post.return_value = self.mock_response 52 | 53 | with self.assertRaises(TransactionCreateError) as context: 54 | self.transaction.create( 55 | self.buy_order_mock, self.session_id_mock, self.amount_mock, self.return_url_mock) 56 | 57 | self.assertIn('Not Authorized', context.exception.message) 58 | self.assertEqual(context.exception.__class__, TransactionCreateError) 59 | 60 | def test_create_exception_buy_order_max_length(self): 61 | with self.assertRaises(TransbankError) as context: 62 | self.transaction.create( 63 | self.token_mock, self.session_id_mock, self.amount_mock, self.return_url_mock) 64 | 65 | self.assertIn( 66 | 'too long, the maximum length', context.exception.message) 67 | self.assertEqual(context.exception.__class__, TransbankError) 68 | 69 | def test_create_exception_session_id_max_length(self): 70 | with self.assertRaises(TransbankError) as context: 71 | self.transaction.create( 72 | self.buy_order_mock, self.token_mock, self.amount_mock, self.return_url_mock) 73 | 74 | self.assertIn( 75 | "'session_id' is too long, the maximum length", context.exception.message) 76 | self.assertEqual(context.exception.__class__, TransbankError) 77 | 78 | def test_create_exception_return_url_max_length(self): 79 | valid_string = string.ascii_letters + string.digits + "-._~" 80 | too_long_url = ''.join(secrets.choice(valid_string) 81 | for _ in range(ApiConstants.RETURN_URL_LENGTH + 1)) 82 | with self.assertRaises(TransbankError) as context: 83 | self.transaction.create( 84 | self.buy_order_mock, self.session_id_mock, self.amount_mock, too_long_url) 85 | 86 | self.assertIn( 87 | "'return_url' is too long, the maximum length", context.exception.message) 88 | self.assertEqual(context.exception.__class__, TransbankError) 89 | 90 | @patch('transbank.common.request_service.requests.put') 91 | def test_commit_transaction_successful(self, mock_put): 92 | self.mock_response.status_code = 200 93 | self.mock_response.text = json.dumps( 94 | responses['commit_status_response']) 95 | mock_put.return_value = self.mock_response 96 | 97 | response = self.transaction.commit(self.token_mock) 98 | 99 | self.assertEqual(response, responses['commit_status_response']) 100 | self.assertTrue(response['response_code'] == 0) 101 | 102 | @patch('transbank.common.request_service.requests.put') 103 | def test_commit_exception_when_authorized(self, mock_put): 104 | self.mock_response.status_code = 422 105 | self.mock_response.text = json.dumps(responses['commit_error']) 106 | mock_put.return_value = self.mock_response 107 | 108 | with self.assertRaises(TransactionCommitError) as context: 109 | self.transaction.commit(self.token_mock) 110 | 111 | self.assertIn( 112 | 'transaction while authorizing', context.exception.message) 113 | self.assertEqual(context.exception.__class__, TransactionCommitError) 114 | 115 | def test_commit_exception_token_max_length(self): 116 | invalid_token = self.token_mock + 'a' 117 | with self.assertRaises(TransbankError) as context: 118 | self.transaction.commit(invalid_token) 119 | 120 | self.assertIn( 121 | "'token' is too long, the maximum length", context.exception.message) 122 | self.assertEqual(context.exception.__class__, TransbankError) 123 | 124 | @patch('transbank.common.request_service.requests.get') 125 | def test_status_transaction_successful(self, mock_get): 126 | self.mock_response.status_code = 200 127 | self.mock_response.text = json.dumps( 128 | responses['commit_status_response']) 129 | mock_get.return_value = self.mock_response 130 | 131 | response = self.transaction.status(self.token_mock) 132 | 133 | self.assertEqual(response, responses['commit_status_response']) 134 | self.assertTrue(response['response_code'] == 0) 135 | 136 | def test_status_exception_token_max_length(self): 137 | invalid_token = self.token_mock + 'a' 138 | with self.assertRaises(TransbankError) as context: 139 | self.transaction.status(invalid_token) 140 | 141 | self.assertIn( 142 | "'token' is too long, the maximum length", context.exception.message) 143 | self.assertEqual(context.exception.__class__, TransbankError) 144 | 145 | @patch('transbank.common.request_service.requests.get') 146 | def test_status_exception_expired_token(self, mock_get): 147 | self.mock_response.status_code = 422 148 | self.mock_response.text = json.dumps(responses['expired_token']) 149 | mock_get.return_value = self.mock_response 150 | 151 | with self.assertRaises(TransactionStatusError) as context: 152 | self.transaction.status(self.token_mock) 153 | 154 | self.assertIn( 155 | 'has passed max time (7 days)', context.exception.message) 156 | self.assertEqual(context.exception.__class__, TransactionStatusError) 157 | 158 | @patch('transbank.common.request_service.requests.post') 159 | def test_refund_transaction_reverse_successful(self, mock_post): 160 | self.mock_response.status_code = 200 161 | self.mock_response.text = json.dumps(responses['reversed_response']) 162 | mock_post.return_value = self.mock_response 163 | 164 | response = self.transaction.refund(self.token_mock, self.amount_mock) 165 | 166 | self.assertTrue(response['type'] == 'REVERSED') 167 | 168 | @patch('transbank.common.request_service.requests.post') 169 | def test_refund_transaction_nullified_successful(self, mock_post): 170 | self.mock_response.status_code = 200 171 | self.mock_response.text = json.dumps(responses['nullified_response']) 172 | mock_post.return_value = self.mock_response 173 | 174 | response = self.transaction.refund(self.token_mock, self.amount_mock) 175 | 176 | self.assertTrue(response['type'] == 'NULLIFIED') 177 | self.assertTrue(response['response_code'] == 0) 178 | 179 | @patch('transbank.common.request_service.requests.post') 180 | def test_refund_exception(self, mock_post): 181 | self.mock_response.status_code = 422 182 | self.mock_response.text = json.dumps(responses['invalid_parameter']) 183 | mock_post.return_value = self.mock_response 184 | 185 | with self.assertRaises(TransactionRefundError) as context: 186 | self.transaction.refund(self.token_mock, self.invalid_amount) 187 | 188 | self.assertIn( 189 | 'Invalid value for parameter', context.exception.message) 190 | self.assertEqual(context.exception.__class__, TransactionRefundError) 191 | 192 | def test_refund_exception_token_max_length(self): 193 | invalid_token = self.token_mock + 'a' 194 | with self.assertRaises(TransbankError) as context: 195 | self.transaction.refund(invalid_token, self.amount_mock) 196 | 197 | self.assertIn( 198 | "'token' is too long, the maximum length", context.exception.message) 199 | self.assertEqual(context.exception.__class__, TransbankError) 200 | 201 | @patch('transbank.common.request_service.requests.put') 202 | def test_capture_transaction_successful(self, mock_put): 203 | self.mock_response.status_code = 200 204 | self.mock_response.text = json.dumps(responses['capture_response']) 205 | mock_put.return_value = self.mock_response 206 | 207 | response = self.transaction.capture(self.token_mock, self.buy_order_mock, self.authorization_code_mock, 208 | self.capture_amount_mock) 209 | 210 | self.assertTrue(response['captured_amount']) 211 | self.assertTrue(response['response_code'] == 0) 212 | 213 | @patch('transbank.common.request_service.requests.put') 214 | def test_capture_exception(self, mock_put): 215 | self.mock_response.status_code = 422 216 | self.mock_response.text = json.dumps(responses['invalid_parameter']) 217 | mock_put.return_value = self.mock_response 218 | 219 | with self.assertRaises(TransactionCaptureError) as context: 220 | self.transaction.capture(self.token_mock, self.buy_order_mock, self.authorization_code_mock, 221 | self.invalid_amount) 222 | 223 | self.assertIn( 224 | 'Invalid value for parameter', context.exception.message) 225 | self.assertEqual(context.exception.__class__, TransactionCaptureError) 226 | 227 | def test_capture_exception_authorization_code_max_length(self): 228 | invalid_authorization_code = self.authorization_code_mock + 'a' 229 | with self.assertRaises(TransbankError) as context: 230 | self.transaction.capture(self.token_mock, self.buy_order_mock, invalid_authorization_code, 231 | self.capture_amount_mock) 232 | 233 | self.assertIn( 234 | "'authorization_code' is too long, the maximum length", context.exception.message) 235 | self.assertEqual(context.exception.__class__, TransbankError) 236 | -------------------------------------------------------------------------------- /tests/webpay/oneclick/test_mall_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | from unittest.mock import Mock 4 | from transbank.webpay.oneclick.mall_transaction import * 5 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 6 | from transbank.common.integration_api_keys import IntegrationApiKeys 7 | from tests.mocks.responses_api_mocks import responses 8 | from unittest.mock import patch 9 | from tests.webpay.test_utils import get_invalid_length_param 10 | 11 | 12 | class OneclickMallTransactionTestCase(unittest.TestCase): 13 | 14 | def setUp(self) -> None: 15 | self.username_mock = 'test_user' 16 | self.tbk_user_mock = '08ed03b1-8fa6-4d7b-b35c-b134e1c5e9ee' 17 | self.parent_buy_order_mock = 'parent_buy_order' 18 | self.installments_number_mock = 0 19 | self.child1_commerce_code = IntegrationCommerceCodes.ONECLICK_MALL_CHILD1 20 | self.amount1_mock = 1000 21 | self.child1_buy_order_mock = 'child_buy_order_1' 22 | self.child2_commerce_code = IntegrationCommerceCodes.ONECLICK_MALL_CHILD2 23 | self.amount2_mock = 2000 24 | self.child2_buy_order_mock = 'child_buy_order_2' 25 | self.return_url_mock = 'https://url_return.com' 26 | self.mock_response = Mock() 27 | self.transaction = MallTransaction.build_for_integration(IntegrationCommerceCodes.ONECLICK_MALL, IntegrationApiKeys.WEBPAY) 28 | self.deferred_transaction = MallTransaction.build_for_integration(IntegrationCommerceCodes.ONECLICK_MALL_DEFERRED, IntegrationApiKeys.WEBPAY) 29 | self.deferred_child_commerce_code = IntegrationCommerceCodes.ONECLICK_MALL_DEFERRED_CHILD1 30 | self.capture_amount_mock = 2000 31 | self.authorization_code_mock = '123456' 32 | 33 | def test_authorize_details(self): 34 | mall_details = MallTransactionAuthorizeDetails(self.child1_commerce_code, self.child1_buy_order_mock, 35 | self.installments_number_mock, self.amount1_mock) 36 | 37 | details = mall_details.add(self.child2_commerce_code, self.child2_buy_order_mock, 38 | self.installments_number_mock, self.amount2_mock) 39 | 40 | self.assertEqual(details.details[0].commerce_code, self.child1_commerce_code) 41 | self.assertEqual(details.details[0].buy_order, self.child1_buy_order_mock) 42 | self.assertEqual(details.details[0].installments_number, self.installments_number_mock) 43 | self.assertEqual(details.details[0].amount, self.amount1_mock) 44 | self.assertEqual(details.details[1].commerce_code, self.child2_commerce_code) 45 | self.assertEqual(details.details[1].buy_order, self.child2_buy_order_mock) 46 | self.assertEqual(details.details[1].installments_number, self.installments_number_mock) 47 | self.assertEqual(details.details[1].amount, self.amount2_mock) 48 | 49 | def get_mall_transaction_details(self): 50 | details = MallTransactionAuthorizeDetails( 51 | self.child1_commerce_code, self.child1_buy_order_mock, self.installments_number_mock, self.amount1_mock) 52 | return details 53 | 54 | @patch('transbank.common.request_service.requests.post') 55 | def test_authorize_transaction_successful(self, mock_post): 56 | self.mock_response.status_code = 200 57 | self.mock_response.text = json.dumps(responses['authorize_response']) 58 | mock_post.return_value = self.mock_response 59 | 60 | response = self.transaction.authorize(self.username_mock, self.tbk_user_mock, self.parent_buy_order_mock, 61 | self.get_mall_transaction_details()) 62 | 63 | self.assertEqual(response, responses['authorize_response']) 64 | 65 | @patch('transbank.common.request_service.requests.post') 66 | def test_deferred_authorize_transaction_successful(self, mock_post): 67 | details = MallTransactionAuthorizeDetails( 68 | self.deferred_child_commerce_code, self.child2_buy_order_mock, self.installments_number_mock, 69 | self.amount2_mock) 70 | self.mock_response.status_code = 200 71 | self.mock_response.text = json.dumps(responses['deferred_authorize_response']) 72 | mock_post.return_value = self.mock_response 73 | 74 | response = self.deferred_transaction.authorize(self.username_mock, self.tbk_user_mock, 75 | self.parent_buy_order_mock, details) 76 | 77 | self.assertEqual(response, responses['deferred_authorize_response']) 78 | 79 | @patch('transbank.common.request_service.requests.post') 80 | def test_authorize_exception(self, mock_post): 81 | self.mock_response.status_code = 500 82 | self.mock_response.text = json.dumps(responses['general_error']) 83 | mock_post.return_value = self.mock_response 84 | 85 | with self.assertRaises(TransactionAuthorizeError) as context: 86 | self.transaction.authorize(self.username_mock, self.tbk_user_mock, self.parent_buy_order_mock, 87 | self.get_mall_transaction_details()) 88 | 89 | self.assertTrue('Internal server error' in context.exception.message) 90 | self.assertEqual(context.exception.__class__, TransactionAuthorizeError) 91 | 92 | def test_authorize_exception_username_max_length(self): 93 | invalid_username = get_invalid_length_param() 94 | with self.assertRaises(TransbankError) as context: 95 | self.transaction.authorize(invalid_username, self.tbk_user_mock, self.parent_buy_order_mock, 96 | self.get_mall_transaction_details()) 97 | 98 | self.assertTrue("'username' is too long" in context.exception.message) 99 | self.assertEqual(context.exception.__class__, TransbankError) 100 | 101 | def test_authorize_exception_tbk_user_max_length(self): 102 | invalid_tbk_user = get_invalid_length_param() 103 | with self.assertRaises(TransbankError) as context: 104 | self.transaction.authorize(self.username_mock, invalid_tbk_user, self.parent_buy_order_mock, 105 | self.get_mall_transaction_details()) 106 | 107 | self.assertTrue("'tbk_user' is too long" in context.exception.message) 108 | self.assertEqual(context.exception.__class__, TransbankError) 109 | 110 | def test_authorize_exception_buy_order_max_length(self): 111 | invalid_parent_buy_order = get_invalid_length_param() 112 | with self.assertRaises(TransbankError) as context: 113 | self.transaction.authorize(self.username_mock, self.tbk_user_mock, invalid_parent_buy_order, 114 | self.get_mall_transaction_details()) 115 | 116 | self.assertTrue("'parent_buy_order' is too long" in context.exception.message) 117 | self.assertEqual(context.exception.__class__, TransbankError) 118 | 119 | def test_authorize_exception_child_commerce_code_max_length(self): 120 | invalid_child_commerce_code = get_invalid_length_param() 121 | details = MallTransactionAuthorizeDetails( 122 | invalid_child_commerce_code, self.child1_buy_order_mock, self.installments_number_mock, self.amount1_mock) 123 | 124 | with self.assertRaises(TransbankError) as context: 125 | self.transaction.authorize(self.username_mock, self.tbk_user_mock, self.parent_buy_order_mock, 126 | details) 127 | 128 | self.assertTrue("'details.commerce_code' is too long" in context.exception.message) 129 | self.assertEqual(context.exception.__class__, TransbankError) 130 | 131 | def test_authorize_exception_child_buy_order_max_length(self): 132 | invalid_child_buy_order = get_invalid_length_param() 133 | details = MallTransactionAuthorizeDetails( 134 | self.child1_commerce_code, invalid_child_buy_order, self.installments_number_mock, self.amount1_mock) 135 | 136 | with self.assertRaises(TransbankError) as context: 137 | self.transaction.authorize(self.username_mock, self.tbk_user_mock, self.parent_buy_order_mock, 138 | details) 139 | 140 | self.assertTrue("'details.buy_order' is too long" in context.exception.message) 141 | self.assertEqual(context.exception.__class__, TransbankError) 142 | 143 | @patch('transbank.common.request_service.requests.put') 144 | def test_capture_transaction_successful(self, mock_put): 145 | self.mock_response.status_code = 200 146 | self.mock_response.text = json.dumps(responses['capture_response']) 147 | mock_put.return_value = self.mock_response 148 | 149 | response = self.deferred_transaction.capture(self.deferred_child_commerce_code, self.child2_buy_order_mock, 150 | self.authorization_code_mock, self.capture_amount_mock) 151 | 152 | self.assertEqual(response, responses['capture_response']) 153 | 154 | @patch('transbank.common.request_service.requests.put') 155 | def test_capture_exception(self, mock_put): 156 | self.mock_response.status_code = 500 157 | self.mock_response.text = json.dumps(responses['general_error']) 158 | mock_put.return_value = self.mock_response 159 | 160 | with self.assertRaises(TransactionCaptureError) as context: 161 | self.deferred_transaction.capture(self.deferred_child_commerce_code, self.child2_buy_order_mock, 162 | self.authorization_code_mock, self.capture_amount_mock) 163 | 164 | self.assertTrue('Internal server error' in context.exception.message) 165 | self.assertEqual(context.exception.__class__, TransactionCaptureError) 166 | 167 | def test_capture_exception_child_commerce_code_max_length(self): 168 | invalid_child_commerce_code = get_invalid_length_param() 169 | 170 | with self.assertRaises(TransbankError) as context: 171 | self.deferred_transaction.capture(invalid_child_commerce_code, self.child2_buy_order_mock, 172 | self.authorization_code_mock, self.capture_amount_mock) 173 | 174 | self.assertTrue("'child_commerce_code' is too long" in context.exception.message) 175 | self.assertEqual(context.exception.__class__, TransbankError) 176 | 177 | def test_capture_exception_child_buy_order_max_length(self): 178 | invalid_child_buy_order = get_invalid_length_param() 179 | 180 | with self.assertRaises(TransbankError) as context: 181 | self.deferred_transaction.capture(self.deferred_child_commerce_code, invalid_child_buy_order, 182 | self.authorization_code_mock, self.capture_amount_mock) 183 | 184 | self.assertTrue("'child_buy_order' is too long" in context.exception.message) 185 | self.assertEqual(context.exception.__class__, TransbankError) 186 | 187 | def test_capture_exception_authorizatioon_code_max_length(self): 188 | invalid_authorization_code = get_invalid_length_param() 189 | 190 | with self.assertRaises(TransbankError) as context: 191 | self.deferred_transaction.capture(self.deferred_child_commerce_code, self.child2_buy_order_mock, 192 | invalid_authorization_code, self.capture_amount_mock) 193 | 194 | self.assertTrue("'authorization_code' is too long" in context.exception.message) 195 | self.assertEqual(context.exception.__class__, TransbankError) 196 | 197 | @patch('transbank.common.request_service.requests.get') 198 | def test_status_transaction_successful(self, mock_get): 199 | self.mock_response.status_code = 200 200 | self.mock_response.text = json.dumps(responses['captured_status_response']) 201 | mock_get.return_value = self.mock_response 202 | 203 | response = self.deferred_transaction.status(self.child2_buy_order_mock) 204 | 205 | self.assertTrue(response['details'][0]['status'], 'CAPTURED') 206 | self.assertEqual(response, responses['captured_status_response']) 207 | 208 | @patch('transbank.common.request_service.requests.get') 209 | def test_status_exception(self, mock_get): 210 | self.mock_response.status_code = 422 211 | self.mock_response.text = json.dumps(responses['buy_order_not_found']) 212 | mock_get.return_value = self.mock_response 213 | 214 | with self.assertRaises(TransactionStatusError) as context: 215 | self.deferred_transaction.status('FakeBuyOrder') 216 | 217 | self.assertTrue("buy order not found" in context.exception.message) 218 | self.assertEqual(context.exception.__class__, TransactionStatusError) 219 | 220 | @patch('transbank.common.request_service.requests.post') 221 | def test_refund_transaction_successful(self, mock_post): 222 | self.mock_response.status_code = 200 223 | self.mock_response.text = json.dumps(responses['reversed_response']) 224 | mock_post.return_value = self.mock_response 225 | 226 | response = self.deferred_transaction.refund(self.parent_buy_order_mock, self.deferred_child_commerce_code, 227 | self.child2_buy_order_mock, self.amount2_mock) 228 | 229 | self.assertTrue(response['type'], 'REVERSED') 230 | 231 | @patch('transbank.common.request_service.requests.post') 232 | def test_refund_exception(self, mock_post): 233 | self.mock_response.status_code = 422 234 | self.mock_response.text = json.dumps(responses['already_refunded_error']) 235 | mock_post.return_value = self.mock_response 236 | 237 | with self.assertRaises(TransactionRefundError) as context: 238 | self.deferred_transaction.refund(self.parent_buy_order_mock, self.deferred_child_commerce_code, 239 | self.child2_buy_order_mock, self.amount2_mock) 240 | 241 | self.assertTrue("Transaction already fully refunded" in context.exception.message) 242 | self.assertEqual(context.exception.__class__, TransactionRefundError) 243 | -------------------------------------------------------------------------------- /tests/webpay/plus/test_mall_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import secrets 4 | import string 5 | from unittest.mock import Mock 6 | from unittest.mock import patch 7 | from transbank.webpay.webpay_plus.mall_transaction import * 8 | from transbank.webpay.webpay_plus.request import * 9 | from transbank.common.integration_commerce_codes import IntegrationCommerceCodes 10 | from transbank.common.integration_api_keys import IntegrationApiKeys 11 | from tests.mocks.responses_api_mocks import responses 12 | from transbank.error.transaction_create_error import TransactionCreateError 13 | 14 | 15 | class TransactionMallTestCase(unittest.TestCase): 16 | 17 | def setUp(self) -> None: 18 | self.mall_buy_order_mock = 'mall_buy_order_mock_123' 19 | self.session_id_mock = 'session_id_mock_123456789' 20 | self.return_url_mock = "https://url_return.com" 21 | self.amount1_mock = 1000 22 | self.child1_commerce_code = IntegrationCommerceCodes.WEBPAY_PLUS_MALL_CHILD1 23 | self.child1_buy_order = 'child_buy_order1_mock_123' 24 | self.amount2_mock = 2000 25 | self.child2_commerce_code = IntegrationCommerceCodes.WEBPAY_PLUS_MALL_CHILD2 26 | self.child2_buy_order = 'child_buy_order2_mock_123' 27 | self.token_mock = '01abf2be20aad1da804aeae1ed3062fb8fba108ee0e07f4d37181f51c3f6714d' 28 | self.mock_response = Mock() 29 | self.transaction = MallTransaction.build_for_integration(IntegrationCommerceCodes.WEBPAY_PLUS_MALL, IntegrationApiKeys.WEBPAY) 30 | self.invalid_amount = -1000 31 | self.authorization_code_mock = '123456' 32 | self.deferred_capture = MallTransaction.build_for_integration(IntegrationCommerceCodes.WEBPAY_PLUS_MALL_DEFERRED, IntegrationApiKeys.WEBPAY) 33 | 34 | def test_create_details(self): 35 | mall_details = MallDetails(self.amount1_mock, self.child1_commerce_code, self.child1_buy_order) 36 | 37 | details = MallTransactionCreateDetails(mall_details.amount, mall_details.commerce_code, mall_details.buy_order).\ 38 | add(self.amount2_mock, self.child2_commerce_code, self.child2_buy_order) 39 | 40 | self.assertEqual(details.details[0].amount, self.amount1_mock) 41 | self.assertEqual(details.details[0].commerce_code, self.child1_commerce_code) 42 | self.assertEqual(details.details[0].buy_order, self.child1_buy_order) 43 | self.assertEqual(details.details[1].amount, self.amount2_mock) 44 | self.assertEqual(details.details[1].commerce_code, self.child2_commerce_code) 45 | self.assertEqual(details.details[1].buy_order, self.child2_buy_order) 46 | 47 | def get_mall_transaction_details(self): 48 | details = MallTransactionCreateDetails(self.amount1_mock, self.child1_commerce_code, self.child1_buy_order) \ 49 | .add(self.amount2_mock, self.child2_commerce_code, self.child2_buy_order) 50 | return details 51 | 52 | @patch('transbank.common.request_service.requests.post') 53 | def test_create_mall_transaction_successful(self, mock_post): 54 | self.mock_response.status_code = 200 55 | self.mock_response.text = json.dumps(responses['create_response']) 56 | mock_post.return_value = self.mock_response 57 | 58 | response = self.transaction.create(self.mall_buy_order_mock, self.session_id_mock, self.return_url_mock, 59 | self.get_mall_transaction_details()) 60 | 61 | self.assertEqual(response, responses['create_response']) 62 | 63 | @patch('transbank.common.request_service.requests.post') 64 | def test_create_mall_exception_not_authorized(self, mock_post): 65 | self.mock_response.status_code = 401 66 | self.mock_response.text = json.dumps(responses['create_error']) 67 | mock_post.return_value = self.mock_response 68 | 69 | with self.assertRaises(TransactionCreateError) as context: 70 | self.transaction.create(self.mall_buy_order_mock, self.session_id_mock, self.return_url_mock, 71 | self.get_mall_transaction_details()) 72 | 73 | self.assertTrue('Not Authorized' in context.exception.message) 74 | self.assertEqual(context.exception.__class__, TransactionCreateError) 75 | 76 | def test_create_mall_exception_buy_order_max_length(self): 77 | with self.assertRaises(TransbankError) as context: 78 | self.transaction.create(self.mall_buy_order_mock+'too_long', self.session_id_mock, self.return_url_mock, 79 | self.get_mall_transaction_details()) 80 | 81 | self.assertTrue("'buy_order' is too long, the maximum length" in context.exception.message) 82 | self.assertEqual(context.exception.__class__, TransbankError) 83 | 84 | def test_create_mall_exception_session_id_max_length(self): 85 | valid_string = string.ascii_letters + string.digits + "-._~" 86 | too_long_session_id = ''.join(secrets.choice(valid_string) for _ in range(ApiConstants.SESSION_ID_LENGTH + 1)) 87 | 88 | with self.assertRaises(TransbankError) as context: 89 | self.transaction.create(self.mall_buy_order_mock, too_long_session_id, self.return_url_mock, 90 | self.get_mall_transaction_details()) 91 | 92 | self.assertTrue("'session_id' is too long, the maximum length" in context.exception.message) 93 | self.assertEqual(context.exception.__class__, TransbankError) 94 | 95 | def test_create_mall_exception_return_url_max_length(self): 96 | valid_string = string.ascii_letters + string.digits + "-._~" 97 | too_long_url = ''.join(secrets.choice(valid_string) for _ in range(ApiConstants.RETURN_URL_LENGTH + 1)) 98 | with self.assertRaises(TransbankError) as context: 99 | self.transaction.create(self.mall_buy_order_mock, self.session_id_mock, too_long_url, 100 | self.get_mall_transaction_details()) 101 | 102 | self.assertTrue("'return_url' is too long, the maximum length" in context.exception.message) 103 | self.assertEqual(context.exception.__class__, TransbankError) 104 | 105 | def test_create_mall_exception_child_buy_order_max_length(self): 106 | valid_string = string.ascii_letters + string.digits + "-._~" 107 | invalid_child_buy_order = ''.join(secrets.choice(valid_string) 108 | for _ in range(ApiConstants.BUY_ORDER_LENGTH + 1)) 109 | 110 | with self.assertRaises(TransbankError) as context: 111 | self.transaction.create(self.mall_buy_order_mock, self.session_id_mock, self.return_url_mock, 112 | MallTransactionCreateDetails(self.amount1_mock, self.child1_commerce_code, 113 | invalid_child_buy_order)) 114 | 115 | self.assertTrue("'details.buy_order' is too long, the maximum length" in context.exception.message) 116 | self.assertEqual(context.exception.__class__, TransbankError) 117 | 118 | def test_create_mall_exception_commerce_code_max_length(self): 119 | with self.assertRaises(TransbankError) as context: 120 | self.transaction.create(self.mall_buy_order_mock, self.session_id_mock, self.return_url_mock, 121 | MallTransactionCreateDetails(self.amount1_mock, self.child1_commerce_code+'123', 122 | self.child1_buy_order)) 123 | 124 | self.assertTrue("'details.commerce_code' is too long, the maximum length" in context.exception.message) 125 | self.assertEqual(context.exception.__class__, TransbankError) 126 | 127 | @patch('transbank.common.request_service.requests.put') 128 | def test_commit_mall_transaction_successful(self, mock_put): 129 | self.mock_response.status_code = 200 130 | self.mock_response.text = json.dumps(responses['commit_mall']) 131 | mock_put.return_value = self.mock_response 132 | 133 | response = self.transaction.commit(self.token_mock) 134 | 135 | self.assertIn('details', response) 136 | self.assertGreaterEqual(len(response['details']), 2) 137 | 138 | for detail in response['details']: 139 | self.assertEqual(detail['response_code'], 0) 140 | 141 | @patch('transbank.common.request_service.requests.put') 142 | def test_commit_mall_exception_when_authorized(self, mock_put): 143 | self.mock_response.status_code = 422 144 | self.mock_response.text = json.dumps(responses['commit_error']) 145 | mock_put.return_value = self.mock_response 146 | 147 | with self.assertRaises(TransactionCommitError) as context: 148 | self.transaction.commit(self.token_mock) 149 | 150 | self.assertTrue('transaction while authorizing' in context.exception.message) 151 | self.assertEqual(context.exception.__class__, TransactionCommitError) 152 | 153 | def test_commit_exception_token_max_length(self): 154 | invalid_token = self.token_mock + 'a' 155 | with self.assertRaises(TransbankError) as context: 156 | self.transaction.commit(invalid_token) 157 | 158 | self.assertTrue("'token' is too long, the maximum length" in context.exception.message) 159 | self.assertEqual(context.exception.__class__, TransbankError) 160 | 161 | @patch('transbank.common.request_service.requests.get') 162 | def test_status_mall_transaction_successful(self, mock_get): 163 | self.mock_response.status_code = 200 164 | self.mock_response.text = json.dumps(responses['commit_mall']) 165 | mock_get.return_value = self.mock_response 166 | 167 | response = self.transaction.status(self.token_mock) 168 | 169 | self.assertEqual(response, responses['commit_mall']) 170 | 171 | def test_status_mall_exception_token_max_length(self): 172 | invalid_token = self.token_mock + 'a' 173 | with self.assertRaises(TransbankError) as context: 174 | self.transaction.status(invalid_token) 175 | 176 | self.assertTrue("'token' is too long, the maximum length" in context.exception.message) 177 | self.assertEqual(context.exception.__class__, TransbankError) 178 | 179 | @patch('transbank.common.request_service.requests.get') 180 | def test_status_mall_exception_expired_token(self, mock_get): 181 | self.mock_response.status_code = 422 182 | self.mock_response.text = json.dumps(responses['expired_token']) 183 | mock_get.return_value = self.mock_response 184 | 185 | with self.assertRaises(TransactionStatusError) as context: 186 | self.transaction.status(self.token_mock) 187 | 188 | self.assertTrue('has passed max time (7 days)' in context.exception.message) 189 | self.assertEqual(context.exception.__class__, TransactionStatusError) 190 | 191 | @patch('transbank.common.request_service.requests.post') 192 | def test_refund_transaction_successful(self, mock_post): 193 | self.mock_response.status_code = 200 194 | self.mock_response.text = json.dumps(responses['nullified_response']) 195 | mock_post.return_value = self.mock_response 196 | 197 | response = self.transaction.refund(self.token_mock, self.child1_buy_order, self.child1_commerce_code, 198 | self.amount1_mock) 199 | 200 | self.assertTrue(response['type'] == 'NULLIFIED') 201 | 202 | @patch('transbank.common.request_service.requests.post') 203 | def test_refund_mall_exception(self, mock_post): 204 | self.mock_response.status_code = 422 205 | self.mock_response.text = json.dumps(responses['bigger_amount_mall']) 206 | mock_post.return_value = self.mock_response 207 | 208 | with self.assertRaises(TransactionRefundError) as context: 209 | self.transaction.refund(self.token_mock, self.child1_buy_order, self.child1_commerce_code, 210 | 1000000) 211 | 212 | self.assertTrue('Amount to refund is bigger than' in context.exception.message) 213 | self.assertEqual(context.exception.__class__, TransactionRefundError) 214 | 215 | def test_refund_mall_exception_token_max_length(self): 216 | invalid_token = self.token_mock + 'a' 217 | with self.assertRaises(TransbankError) as context: 218 | self.transaction.refund(invalid_token, self.child1_buy_order, self.child1_commerce_code, self.amount1_mock) 219 | 220 | self.assertTrue("'token' is too long, the maximum length" in context.exception.message) 221 | self.assertEqual(context.exception.__class__, TransbankError) 222 | 223 | def test_refund_mall_exception_child_commerce_code_max_length(self): 224 | with self.assertRaises(TransbankError) as context: 225 | self.transaction.refund(self.token_mock, self.child1_buy_order, self.child1_commerce_code+'123', 226 | self.amount1_mock) 227 | 228 | self.assertTrue("'child_commerce_code' is too long, the maximum length" in context.exception.message) 229 | self.assertEqual(context.exception.__class__, TransbankError) 230 | 231 | def test_refund_mall_exception_child_buy_order_max_length(self): 232 | with self.assertRaises(TransbankError) as context: 233 | self.transaction.refund(self.token_mock, self.child1_buy_order*2, self.child1_commerce_code, 234 | self.amount1_mock) 235 | 236 | self.assertTrue("'child_buy_order' is too long, the maximum length" in context.exception.message) 237 | self.assertEqual(context.exception.__class__, TransbankError) 238 | 239 | @patch('transbank.common.request_service.requests.get') 240 | def test_status_mall_deferred_transaction_successful(self, mock_get): 241 | self.mock_response.status_code = 200 242 | self.mock_response.text = json.dumps(responses['status_mall_deferred']) 243 | mock_get.return_value = self.mock_response 244 | 245 | response = self.deferred_capture.status(self.token_mock) 246 | 247 | for detail in response['details']: 248 | self.assertIsNotNone(detail['capture_expiration_date']) 249 | 250 | @patch('transbank.common.request_service.requests.put') 251 | def test_capture_mall_transaction_successful(self, mock_put): 252 | self.mock_response.status_code = 200 253 | self.mock_response.text = json.dumps(responses['capture_response']) 254 | mock_put.return_value = self.mock_response 255 | 256 | response = self.deferred_capture.capture(self.child1_commerce_code, self.token_mock, self.child1_buy_order, 257 | self.authorization_code_mock, self.amount1_mock) 258 | 259 | self.assertTrue(response['captured_amount']) 260 | self.assertTrue(response['response_code'] == 0) 261 | 262 | @patch('transbank.common.request_service.requests.put') 263 | def test_capture_mall_exception(self, mock_put): 264 | self.mock_response.status_code = 422 265 | self.mock_response.text = json.dumps(responses['invalid_parameter']) 266 | mock_put.return_value = self.mock_response 267 | 268 | with self.assertRaises(TransactionCaptureError) as context: 269 | self.deferred_capture.capture(self.child1_commerce_code, self.token_mock, self.child1_buy_order, 270 | self.authorization_code_mock, self.invalid_amount) 271 | 272 | self.assertTrue('Invalid value for parameter' in context.exception.message) 273 | self.assertEqual(context.exception.__class__, TransactionCaptureError) 274 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "cdbb901a9aee9e883127f93025761642fd6a105eeea8a1307cb6eb4c4d349d37" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", 22 | "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==2025.6.15" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", 30 | "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", 31 | "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", 32 | "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", 33 | "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", 34 | "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", 35 | "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", 36 | "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", 37 | "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", 38 | "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", 39 | "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", 40 | "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", 41 | "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", 42 | "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", 43 | "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", 44 | "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", 45 | "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", 46 | "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", 47 | "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", 48 | "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", 49 | "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", 50 | "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", 51 | "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", 52 | "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", 53 | "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", 54 | "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", 55 | "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", 56 | "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", 57 | "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", 58 | "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", 59 | "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", 60 | "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", 61 | "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", 62 | "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", 63 | "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", 64 | "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", 65 | "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", 66 | "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", 67 | "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", 68 | "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", 69 | "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", 70 | "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", 71 | "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", 72 | "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", 73 | "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", 74 | "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", 75 | "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", 76 | "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", 77 | "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", 78 | "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", 79 | "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", 80 | "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", 81 | "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", 82 | "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", 83 | "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", 84 | "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", 85 | "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", 86 | "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", 87 | "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", 88 | "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", 89 | "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", 90 | "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", 91 | "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", 92 | "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", 93 | "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", 94 | "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", 95 | "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", 96 | "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", 97 | "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", 98 | "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", 99 | "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", 100 | "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", 101 | "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", 102 | "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", 103 | "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", 104 | "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", 105 | "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", 106 | "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", 107 | "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", 108 | "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", 109 | "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", 110 | "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", 111 | "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", 112 | "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", 113 | "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", 114 | "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", 115 | "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", 116 | "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", 117 | "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", 118 | "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", 119 | "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", 120 | "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" 121 | ], 122 | "markers": "python_version >= '3.7'", 123 | "version": "==3.4.2" 124 | }, 125 | "idna": { 126 | "hashes": [ 127 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 128 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 129 | ], 130 | "markers": "python_version >= '3.6'", 131 | "version": "==3.10" 132 | }, 133 | "marshmallow": { 134 | "hashes": [ 135 | "sha256:3b6e80aac299a7935cfb97ed01d1854fb90b5079430969af92118ea1b12a8d55", 136 | "sha256:e7b0528337e9990fd64950f8a6b3a1baabed09ad17a0dfb844d701151f92d203" 137 | ], 138 | "index": "pypi", 139 | "markers": "python_version >= '3.9'", 140 | "version": "==4.0.0" 141 | }, 142 | "mock": { 143 | "hashes": [ 144 | "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", 145 | "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f" 146 | ], 147 | "index": "pypi", 148 | "markers": "python_version >= '3.6'", 149 | "version": "==5.2.0" 150 | }, 151 | "requests": { 152 | "hashes": [ 153 | "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", 154 | "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" 155 | ], 156 | "index": "pypi", 157 | "markers": "python_version >= '3.8'", 158 | "version": "==2.32.4" 159 | }, 160 | "setuptools": { 161 | "hashes": [ 162 | "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", 163 | "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" 164 | ], 165 | "index": "pypi", 166 | "markers": "python_version >= '3.9'", 167 | "version": "==80.9.0" 168 | }, 169 | "urllib3": { 170 | "hashes": [ 171 | "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", 172 | "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" 173 | ], 174 | "markers": "python_version >= '3.9'", 175 | "version": "==2.5.0" 176 | } 177 | }, 178 | "develop": { 179 | "astroid": { 180 | "hashes": [ 181 | "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", 182 | "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce" 183 | ], 184 | "markers": "python_full_version >= '3.9.0'", 185 | "version": "==3.3.10" 186 | }, 187 | "asttokens": { 188 | "hashes": [ 189 | "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", 190 | "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2" 191 | ], 192 | "markers": "python_version >= '3.8'", 193 | "version": "==3.0.0" 194 | }, 195 | "certifi": { 196 | "hashes": [ 197 | "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", 198 | "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" 199 | ], 200 | "markers": "python_version >= '3.7'", 201 | "version": "==2025.6.15" 202 | }, 203 | "charset-normalizer": { 204 | "hashes": [ 205 | "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", 206 | "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", 207 | "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", 208 | "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", 209 | "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", 210 | "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", 211 | "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", 212 | "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", 213 | "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", 214 | "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", 215 | "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", 216 | "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", 217 | "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", 218 | "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", 219 | "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", 220 | "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", 221 | "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", 222 | "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", 223 | "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", 224 | "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", 225 | "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", 226 | "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", 227 | "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", 228 | "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", 229 | "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", 230 | "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", 231 | "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", 232 | "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", 233 | "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", 234 | "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", 235 | "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", 236 | "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", 237 | "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", 238 | "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", 239 | "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", 240 | "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", 241 | "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", 242 | "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", 243 | "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", 244 | "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", 245 | "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", 246 | "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", 247 | "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", 248 | "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", 249 | "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", 250 | "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", 251 | "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", 252 | "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", 253 | "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", 254 | "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", 255 | "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", 256 | "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", 257 | "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", 258 | "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", 259 | "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", 260 | "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", 261 | "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", 262 | "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", 263 | "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", 264 | "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", 265 | "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", 266 | "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", 267 | "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", 268 | "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", 269 | "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", 270 | "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", 271 | "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", 272 | "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", 273 | "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", 274 | "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", 275 | "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", 276 | "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", 277 | "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", 278 | "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", 279 | "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", 280 | "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", 281 | "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", 282 | "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", 283 | "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", 284 | "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", 285 | "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", 286 | "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", 287 | "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", 288 | "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", 289 | "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", 290 | "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", 291 | "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", 292 | "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", 293 | "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", 294 | "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", 295 | "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", 296 | "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" 297 | ], 298 | "markers": "python_version >= '3.7'", 299 | "version": "==3.4.2" 300 | }, 301 | "coverage": { 302 | "extras": [ 303 | "toml" 304 | ], 305 | "hashes": [ 306 | "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71", 307 | "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", 308 | "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", 309 | "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", 310 | "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", 311 | "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", 312 | "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", 313 | "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", 314 | "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", 315 | "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", 316 | "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", 317 | "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", 318 | "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", 319 | "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", 320 | "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", 321 | "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", 322 | "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", 323 | "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", 324 | "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", 325 | "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", 326 | "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385", 327 | "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", 328 | "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58", 329 | "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55", 330 | "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", 331 | "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", 332 | "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", 333 | "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", 334 | "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", 335 | "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", 336 | "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", 337 | "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed", 338 | "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", 339 | "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", 340 | "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", 341 | "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", 342 | "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951", 343 | "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", 344 | "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", 345 | "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", 346 | "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", 347 | "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", 348 | "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d", 349 | "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", 350 | "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", 351 | "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", 352 | "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7", 353 | "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", 354 | "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", 355 | "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", 356 | "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", 357 | "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", 358 | "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", 359 | "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", 360 | "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", 361 | "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b", 362 | "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", 363 | "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", 364 | "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", 365 | "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", 366 | "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", 367 | "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", 368 | "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244", 369 | "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", 370 | "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", 371 | "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", 372 | "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3" 373 | ], 374 | "index": "pypi", 375 | "markers": "python_version >= '3.9'", 376 | "version": "==7.9.1" 377 | }, 378 | "decorator": { 379 | "hashes": [ 380 | "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", 381 | "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" 382 | ], 383 | "markers": "python_version >= '3.8'", 384 | "version": "==5.2.1" 385 | }, 386 | "dill": { 387 | "hashes": [ 388 | "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", 389 | "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" 390 | ], 391 | "markers": "python_version >= '3.8'", 392 | "version": "==0.4.0" 393 | }, 394 | "docutils": { 395 | "hashes": [ 396 | "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", 397 | "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" 398 | ], 399 | "index": "pypi", 400 | "markers": "python_version >= '3.9'", 401 | "version": "==0.21.2" 402 | }, 403 | "executing": { 404 | "hashes": [ 405 | "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", 406 | "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755" 407 | ], 408 | "markers": "python_version >= '3.8'", 409 | "version": "==2.2.0" 410 | }, 411 | "idna": { 412 | "hashes": [ 413 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 414 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 415 | ], 416 | "markers": "python_version >= '3.6'", 417 | "version": "==3.10" 418 | }, 419 | "iniconfig": { 420 | "hashes": [ 421 | "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", 422 | "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" 423 | ], 424 | "markers": "python_version >= '3.8'", 425 | "version": "==2.1.0" 426 | }, 427 | "ipython": { 428 | "hashes": [ 429 | "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", 430 | "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8" 431 | ], 432 | "index": "pypi", 433 | "markers": "python_version >= '3.11'", 434 | "version": "==9.3.0" 435 | }, 436 | "ipython-pygments-lexers": { 437 | "hashes": [ 438 | "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", 439 | "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c" 440 | ], 441 | "markers": "python_version >= '3.8'", 442 | "version": "==1.1.1" 443 | }, 444 | "isort": { 445 | "hashes": [ 446 | "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", 447 | "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615" 448 | ], 449 | "markers": "python_full_version >= '3.9.0'", 450 | "version": "==6.0.1" 451 | }, 452 | "jedi": { 453 | "hashes": [ 454 | "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", 455 | "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9" 456 | ], 457 | "markers": "python_version >= '3.6'", 458 | "version": "==0.19.2" 459 | }, 460 | "matplotlib-inline": { 461 | "hashes": [ 462 | "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", 463 | "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" 464 | ], 465 | "markers": "python_version >= '3.8'", 466 | "version": "==0.1.7" 467 | }, 468 | "mccabe": { 469 | "hashes": [ 470 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 471 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 472 | ], 473 | "markers": "python_version >= '3.6'", 474 | "version": "==0.7.0" 475 | }, 476 | "packaging": { 477 | "hashes": [ 478 | "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", 479 | "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" 480 | ], 481 | "markers": "python_version >= '3.8'", 482 | "version": "==25.0" 483 | }, 484 | "parso": { 485 | "hashes": [ 486 | "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", 487 | "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" 488 | ], 489 | "markers": "python_version >= '3.6'", 490 | "version": "==0.8.4" 491 | }, 492 | "pexpect": { 493 | "hashes": [ 494 | "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", 495 | "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" 496 | ], 497 | "markers": "sys_platform != 'win32' and sys_platform != 'emscripten'", 498 | "version": "==4.9.0" 499 | }, 500 | "platformdirs": { 501 | "hashes": [ 502 | "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", 503 | "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4" 504 | ], 505 | "markers": "python_version >= '3.9'", 506 | "version": "==4.3.8" 507 | }, 508 | "pluggy": { 509 | "hashes": [ 510 | "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", 511 | "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" 512 | ], 513 | "markers": "python_version >= '3.9'", 514 | "version": "==1.6.0" 515 | }, 516 | "prompt-toolkit": { 517 | "hashes": [ 518 | "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", 519 | "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed" 520 | ], 521 | "markers": "python_version >= '3.8'", 522 | "version": "==3.0.51" 523 | }, 524 | "ptyprocess": { 525 | "hashes": [ 526 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 527 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 528 | ], 529 | "version": "==0.7.0" 530 | }, 531 | "pure-eval": { 532 | "hashes": [ 533 | "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", 534 | "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" 535 | ], 536 | "version": "==0.2.3" 537 | }, 538 | "pygments": { 539 | "hashes": [ 540 | "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", 541 | "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" 542 | ], 543 | "markers": "python_version >= '3.8'", 544 | "version": "==2.19.2" 545 | }, 546 | "pylint": { 547 | "hashes": [ 548 | "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", 549 | "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d" 550 | ], 551 | "index": "pypi", 552 | "markers": "python_full_version >= '3.9.0'", 553 | "version": "==3.3.7" 554 | }, 555 | "pytest": { 556 | "hashes": [ 557 | "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", 558 | "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c" 559 | ], 560 | "index": "pypi", 561 | "markers": "python_version >= '3.9'", 562 | "version": "==8.4.1" 563 | }, 564 | "pytest-cov": { 565 | "hashes": [ 566 | "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", 567 | "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5" 568 | ], 569 | "index": "pypi", 570 | "markers": "python_version >= '3.9'", 571 | "version": "==6.2.1" 572 | }, 573 | "requests": { 574 | "hashes": [ 575 | "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", 576 | "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" 577 | ], 578 | "index": "pypi", 579 | "markers": "python_version >= '3.8'", 580 | "version": "==2.32.4" 581 | }, 582 | "requests-mock": { 583 | "hashes": [ 584 | "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", 585 | "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" 586 | ], 587 | "index": "pypi", 588 | "markers": "python_version >= '3.5'", 589 | "version": "==1.12.1" 590 | }, 591 | "stack-data": { 592 | "hashes": [ 593 | "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", 594 | "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" 595 | ], 596 | "version": "==0.6.3" 597 | }, 598 | "tomlkit": { 599 | "hashes": [ 600 | "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", 601 | "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" 602 | ], 603 | "markers": "python_version >= '3.8'", 604 | "version": "==0.13.3" 605 | }, 606 | "traitlets": { 607 | "hashes": [ 608 | "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", 609 | "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" 610 | ], 611 | "markers": "python_version >= '3.8'", 612 | "version": "==5.14.3" 613 | }, 614 | "urllib3": { 615 | "hashes": [ 616 | "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", 617 | "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" 618 | ], 619 | "markers": "python_version >= '3.9'", 620 | "version": "==2.5.0" 621 | }, 622 | "wcwidth": { 623 | "hashes": [ 624 | "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", 625 | "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" 626 | ], 627 | "version": "==0.2.13" 628 | } 629 | } 630 | } 631 | --------------------------------------------------------------------------------