python-redsys 1.2.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_redsys-2.0.0.dist-info/METADATA +214 -0
- python_redsys-2.0.0.dist-info/RECORD +20 -0
- {python_redsys-1.2.2.dist-info → python_redsys-2.0.0.dist-info}/WHEEL +2 -1
- python_redsys-2.0.0.dist-info/licenses/LICENSE +202 -0
- python_redsys-2.0.0.dist-info/licenses/NOTICE +7 -0
- python_redsys-2.0.0.dist-info/top_level.txt +1 -0
- redsys/__init__.py +3 -2
- redsys/api/__init__.py +1 -0
- redsys/api/cof.py +29 -0
- redsys/api/currencies.py +84 -0
- redsys/api/languages.py +67 -0
- redsys/api/sca.py +23 -0
- redsys/api/transactions.py +46 -0
- redsys/clients.py +272 -0
- redsys/crypto.py +123 -0
- redsys/exceptions.py +19 -0
- redsys/models.py +340 -0
- redsys/py.typed +1 -0
- redsys/request.py +54 -157
- redsys/threeds.py +67 -0
- python_redsys-1.2.2.dist-info/METADATA +0 -122
- python_redsys-1.2.2.dist-info/RECORD +0 -13
- python_redsys-1.2.2.dist-info/licenses/LICENSE +0 -25
- redsys/client.py +0 -105
- redsys/constants.py +0 -113
- redsys/response.py +0 -149
- redsys/tests/__init__.py +0 -0
- redsys/tests/test_client.py +0 -89
- redsys/tests/test_request.py +0 -74
- redsys/tests/test_response.py +0 -39
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-redsys
|
|
3
|
-
Version: 1.2.2
|
|
4
|
-
Summary: A simple, lightweight client to handle payments through the RedSys platform.
|
|
5
|
-
License: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Author: David Díaz
|
|
8
|
-
Author-email: d.diazp@gmail.com
|
|
9
|
-
Requires-Python: >=3.8,<4.0
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
-
Requires-Dist: pycryptodome (>=3.19.1,<4.0.0)
|
|
23
|
-
Project-URL: Homepage, https://github.com/ddiazpinto/python-redsys
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# Welcome to python-redsys!
|
|
27
|
-
|
|
28
|
-
[](https://github.com/ddiazpinto/python-redsys/actions/workflows/python-app.yml?query=branch%3Arelease%2F1.x) [](https://pypi.org/project/python-redsys/)  [](https://github.com/ddiazpinto/python-redsys/blob/release/1.x/LICENSE)
|
|
29
|
-
|
|
30
|
-
A simple, lightweight client to handle payments through the RedSys platform, using its *redirect connection* (browser form POST).
|
|
31
|
-
|
|
32
|
-
The library only builds the signed request and verifies/parses the response. It makes no network or browser calls of its own: the payment form is submitted from the cardholder's browser.
|
|
33
|
-
|
|
34
|
-
## Installation
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pip install "python-redsys>=1.2,<2"
|
|
38
|
-
|
|
39
|
-
# or with Poetry
|
|
40
|
-
poetry add "python-redsys@^1.2"
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Requires Python 3.8+. Runtime dependency: `pycryptodome`.
|
|
44
|
-
|
|
45
|
-
## Usage
|
|
46
|
-
|
|
47
|
-
The client prepares a request from a dict of parameters and verifies the response. Parameter values are validated on construction (`ValueError` on an unknown parameter or an invalid value, `TypeError` when `amount` is not a `Decimal`); `order` has 12 characters (4 digits followed by 8 alphanumerics), and `amount` is a `Decimal`, serialized to integer minor units on the wire. The allowed values for `currency`, `transaction_type` and `consumer_language` are in `redsys.constants`.
|
|
48
|
-
|
|
49
|
-
## Using *redirect connection*
|
|
50
|
-
|
|
51
|
-
### 1. Build a signed request
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
from decimal import Decimal, ROUND_HALF_UP
|
|
55
|
-
from redsys.client import RedirectClient
|
|
56
|
-
from redsys.constants import EUR, STANDARD_PAYMENT
|
|
57
|
-
|
|
58
|
-
client = RedirectClient(secret_key="sq7HjrUOBfKmC576ILgskD5srU870gJ7")
|
|
59
|
-
|
|
60
|
-
post_parameters = client.prepare_request({
|
|
61
|
-
"merchant_code": "999008881",
|
|
62
|
-
"terminal": "1",
|
|
63
|
-
"transaction_type": STANDARD_PAYMENT,
|
|
64
|
-
"currency": EUR,
|
|
65
|
-
"order": "000000000001",
|
|
66
|
-
# The amount is a Decimal; pre-quantize to two decimals.
|
|
67
|
-
"amount": Decimal("10.56489").quantize(Decimal(".01"), ROUND_HALF_UP),
|
|
68
|
-
"merchant_url": "https://example.com/redsys/response",
|
|
69
|
-
"url_ok": "https://example.com/ok",
|
|
70
|
-
"url_ko": "https://example.com/ko",
|
|
71
|
-
"merchant_name": "Example Commerce",
|
|
72
|
-
"product_description": "Products of Example Commerce",
|
|
73
|
-
}) # Ds_SignatureVersion / Ds_MerchantParameters / Ds_Signature
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The secret key, merchant code `999008881` and terminal `1` are the RedSys public test merchant's. `Ds_MerchantParameters` and `Ds_Signature` are returned as `bytes`; decode them before rendering them in an HTML form.
|
|
77
|
-
|
|
78
|
-
Bizum: add `"payment_method": "z"` (and optionally `"mobile_number"`) to the parameters.
|
|
79
|
-
|
|
80
|
-
### 2. Communication step
|
|
81
|
-
|
|
82
|
-
Auto-submit an HTML form (`method="POST"`) with the three `post_parameters` fields to the RedSys endpoint:
|
|
83
|
-
|
|
84
|
-
- Test: `https://sis-t.redsys.es:25443/sis/realizarPago`
|
|
85
|
-
- Production: `https://sis.redsys.es/sis/realizarPago`
|
|
86
|
-
|
|
87
|
-
RedSys notifies the result both as a server-to-server POST to `merchant_url` and as a browser redirect to `url_ok` / `url_ko`.
|
|
88
|
-
|
|
89
|
-
### 3. Verify and read the response
|
|
90
|
-
|
|
91
|
-
`create_response()` takes the received `Ds_Signature` and `Ds_MerchantParameters` and raises `ValueError` on signature mismatch.
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
response = client.create_response(signature, merchant_parameters) # as RedSys posted them back
|
|
95
|
-
|
|
96
|
-
if response.is_paid:
|
|
97
|
-
... # actions after a successful payment
|
|
98
|
-
else:
|
|
99
|
-
raise Exception(f"declined with code {response.code}")
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Response predicates (per the RedSys documentation):
|
|
103
|
-
|
|
104
|
-
- `response.is_paid`: response code between 0 and 99.
|
|
105
|
-
- `response.is_canceled`: response code 400.
|
|
106
|
-
- `response.is_refunded`: response code 900.
|
|
107
|
-
- `response.is_authorized`: paid, refunded or canceled.
|
|
108
|
-
|
|
109
|
-
`response.code` is the RedSys code as an `int`, and `response.message` its Spanish description (`None` for a code without one). Decoded fields (`response.order`, `response.amount` as a `Decimal`, ...) read as attributes, `None` when absent; `response.parameters` holds every decoded parameter: listed ones under their attribute name (`amount`), unlisted ones under their original `Ds_*` key.
|
|
110
|
-
|
|
111
|
-
## Signature version
|
|
112
|
-
|
|
113
|
-
The 1.x line signs with `HMAC_SHA256_V1` (3DES + HMAC-SHA256), the legacy scheme still accepted by RedSys. `HMAC_SHA512_V1` requires 2.x.
|
|
114
|
-
|
|
115
|
-
## Support
|
|
116
|
-
|
|
117
|
-
The 1.x line receives fixes until 2026-12-31.
|
|
118
|
-
|
|
119
|
-
## Contributing
|
|
120
|
-
|
|
121
|
-
Contributions that keep the *lightweight* philosophy are welcome.
|
|
122
|
-
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
redsys/__init__.py,sha256=k234cuEigYkeydQvG5NyAl4pPAyCQSjL65UvOkeTak8,138
|
|
2
|
-
redsys/client.py,sha256=ervLX4FbzDfWjh_SO2SFyY9IT0PZ5aIcVZdYy4mUO7Q,3937
|
|
3
|
-
redsys/constants.py,sha256=lJYgbmikxh0b9iVM4M5XFWBWVvdFY27OkcXSLluHSsQ,2497
|
|
4
|
-
redsys/request.py,sha256=IEzlRi34JgDBRRyaBHtIrAZ38fADL977RdBFaeWDdbY,5304
|
|
5
|
-
redsys/response.py,sha256=YDihLSIE388Ib53qr9PUtGsWqLqj1zFdjbgn9x_i-8A,5585
|
|
6
|
-
redsys/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
redsys/tests/test_client.py,sha256=oND8W9y6O6MkDLNxnBpd8UDShwKWoAW3SgKmWDOXHrw,3858
|
|
8
|
-
redsys/tests/test_request.py,sha256=FONigjMNdaug2DwillYKCLPK-gzIYZSVUZmcm9QkCms,3110
|
|
9
|
-
redsys/tests/test_response.py,sha256=K1QP7eRyBum88LXG6IOQtno8FZNxoVHUv-GJU83k5OI,1777
|
|
10
|
-
python_redsys-1.2.2.dist-info/METADATA,sha256=PXqNlioj4tNFrfUXhiPZ1P77pgIJne39Q_oD4sXto4U,5517
|
|
11
|
-
python_redsys-1.2.2.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
12
|
-
python_redsys-1.2.2.dist-info/licenses/LICENSE,sha256=Ku6mJcUOcHd2aZ4o2Jh4Tjkx1oZT-iT0VCyUNvm2IXU,1257
|
|
13
|
-
python_redsys-1.2.2.dist-info/RECORD,,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
The original development of python-redsys is copyrighted to David Díaz.
|
|
4
|
-
Copyright (c) 2016 David Díaz
|
|
5
|
-
|
|
6
|
-
Extensions to python-redsys are copyrighted to Andrés Reverón Molina.
|
|
7
|
-
Copyright (c) 2021 Andrés Reverón Molina
|
|
8
|
-
|
|
9
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
-
in the Software without restriction, including without limitation the rights
|
|
12
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
-
furnished to do so, subject to the following conditions:
|
|
15
|
-
|
|
16
|
-
The above copyright notice and this permission notice shall be included in all
|
|
17
|
-
copies or substantial portions of the Software.
|
|
18
|
-
|
|
19
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
redsys/client.py
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import hashlib
|
|
3
|
-
import hmac
|
|
4
|
-
import json
|
|
5
|
-
import re
|
|
6
|
-
from abc import ABC
|
|
7
|
-
from abc import abstractmethod
|
|
8
|
-
from typing import Any
|
|
9
|
-
from typing import Dict
|
|
10
|
-
|
|
11
|
-
from Crypto.Cipher import DES3
|
|
12
|
-
|
|
13
|
-
from redsys.request import Request
|
|
14
|
-
from redsys.response import Response
|
|
15
|
-
|
|
16
|
-
# Calculated parameters
|
|
17
|
-
SIGNATURE_VERSION = "Ds_SignatureVersion"
|
|
18
|
-
MERCHANT_PARAMETERS = "Ds_MerchantParameters"
|
|
19
|
-
SIGNATURE = "Ds_Signature"
|
|
20
|
-
DEFAULT_SIGNATURE_VERSION = "HMAC_SHA256_V1"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Client(ABC):
|
|
24
|
-
"""
|
|
25
|
-
Abstract class from which RedirectClient inherits.
|
|
26
|
-
It implements methods that may be used in future clients(i.e. rest).
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self, secret_key: str):
|
|
30
|
-
self.secret_key: bytes = secret_key.encode()
|
|
31
|
-
|
|
32
|
-
@abstractmethod
|
|
33
|
-
def create_response(self, signature, parameters):
|
|
34
|
-
raise NotImplementedError
|
|
35
|
-
|
|
36
|
-
@abstractmethod
|
|
37
|
-
def prepare_request(self, request):
|
|
38
|
-
raise NotImplementedError
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def encode_parameters(parameters: Dict[str, Any]) -> bytes:
|
|
42
|
-
"""Encodes the merchant parameters in base64"""
|
|
43
|
-
return base64.b64encode(json.dumps(parameters).encode())
|
|
44
|
-
|
|
45
|
-
@staticmethod
|
|
46
|
-
def decode_parameters(parameters: bytes) -> Dict[str, Any]:
|
|
47
|
-
"""Decodes the merchant parameters from base64"""
|
|
48
|
-
return json.loads(base64.b64decode(parameters).decode())
|
|
49
|
-
|
|
50
|
-
@staticmethod
|
|
51
|
-
def sign_hmac256(encrypted_order: bytes, merchant_parameters: bytes) -> bytes:
|
|
52
|
-
"""
|
|
53
|
-
Generates the encrypted signature using the 3DES-encrypted order
|
|
54
|
-
and base64-encoded merchant parameters
|
|
55
|
-
"""
|
|
56
|
-
signature = hmac.new(encrypted_order, merchant_parameters, hashlib.sha256).digest()
|
|
57
|
-
return base64.b64encode(signature)
|
|
58
|
-
|
|
59
|
-
def encrypt_3DES(self, order: str) -> bytes:
|
|
60
|
-
"""Encrypts(3DES algorithm) the payment order using the secret key"""
|
|
61
|
-
cipher = DES3.new(base64.b64decode(self.secret_key), DES3.MODE_CBC, IV=b"\0\0\0\0\0\0\0\0")
|
|
62
|
-
# the cipher needs to be passed 16 bytes,
|
|
63
|
-
# so "order" must be 16 bytes long,
|
|
64
|
-
# therefore we left-justify adding ceros
|
|
65
|
-
return cipher.encrypt(order.encode().ljust(16, b"\0"))
|
|
66
|
-
|
|
67
|
-
def generate_signature(self, order: str, merchant_parameters: bytes) -> bytes:
|
|
68
|
-
return self.sign_hmac256(self.encrypt_3DES(order), merchant_parameters)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class RedirectClient(Client):
|
|
72
|
-
def prepare_request(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
73
|
-
"""Takes the merchant parameters and returns the necessary parameters
|
|
74
|
-
to make the POST request to Redsys"""
|
|
75
|
-
request = Request(parameters)
|
|
76
|
-
merchant_parameters = self.encode_parameters(request.prepare_parameters())
|
|
77
|
-
signature = self.generate_signature(request.order, merchant_parameters)
|
|
78
|
-
return {
|
|
79
|
-
SIGNATURE_VERSION: DEFAULT_SIGNATURE_VERSION,
|
|
80
|
-
MERCHANT_PARAMETERS: merchant_parameters,
|
|
81
|
-
SIGNATURE: signature,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
def create_response(
|
|
85
|
-
self,
|
|
86
|
-
signature: str,
|
|
87
|
-
merchant_parameters: str,
|
|
88
|
-
) -> Response:
|
|
89
|
-
"""
|
|
90
|
-
Decodes the Redsys response to check for validity.
|
|
91
|
-
Checks if the received signature corresponds to the sent signature.
|
|
92
|
-
|
|
93
|
-
Both the `signature` and `merchant parameters` are plain strings, not bytes.
|
|
94
|
-
"""
|
|
95
|
-
decoded_parameters = self.decode_parameters(merchant_parameters.encode())
|
|
96
|
-
response = Response(decoded_parameters)
|
|
97
|
-
calculated_signature = self.generate_signature(response.order, merchant_parameters.encode())
|
|
98
|
-
# Remove any non-alphanumeric characters from the signature
|
|
99
|
-
not_alphanumeric = re.compile("[^a-zA-Z0-9]")
|
|
100
|
-
safe_signature = re.sub(not_alphanumeric, "", signature)
|
|
101
|
-
|
|
102
|
-
safe_calculated_signature = re.sub(not_alphanumeric, "", calculated_signature.decode())
|
|
103
|
-
if safe_signature != safe_calculated_signature:
|
|
104
|
-
raise ValueError("The provided signature is not valid.")
|
|
105
|
-
return response
|
redsys/constants.py
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Currencies
|
|
2
|
-
""" Euro """
|
|
3
|
-
EUR = 978
|
|
4
|
-
""" United States dollar """
|
|
5
|
-
USD = 840
|
|
6
|
-
""" Pound sterling """
|
|
7
|
-
GBP = 826
|
|
8
|
-
""" Japanese yen """
|
|
9
|
-
JPY = 392
|
|
10
|
-
""" Argentine peso """
|
|
11
|
-
ARS = 32
|
|
12
|
-
""" Canadian dollar """
|
|
13
|
-
CAD = 124
|
|
14
|
-
""" Chilean Peso """
|
|
15
|
-
CLP = 152
|
|
16
|
-
""" Colombian peso """
|
|
17
|
-
COP = 170
|
|
18
|
-
""" Indian rupee """
|
|
19
|
-
INR = 356
|
|
20
|
-
""" Mexican peso """
|
|
21
|
-
MXN = 484
|
|
22
|
-
""" Peruvian Sol """
|
|
23
|
-
PEN = 604
|
|
24
|
-
""" Swiss franc """
|
|
25
|
-
CHF = 756
|
|
26
|
-
""" Brazilian real """
|
|
27
|
-
BRL = 986
|
|
28
|
-
""" Venezuelan bolivar """
|
|
29
|
-
VEF = 937
|
|
30
|
-
""" Turkish lira """
|
|
31
|
-
TRY = 949
|
|
32
|
-
|
|
33
|
-
CURRENCIES = [EUR, USD, GBP, JPY, ARS, CAD, CLP, COP, INR, MXN, PEN, CHF, BRL, VEF, TRY]
|
|
34
|
-
|
|
35
|
-
# Languages
|
|
36
|
-
CUSTOMER = "000"
|
|
37
|
-
SPANISH = "001"
|
|
38
|
-
ENGLISH = "002"
|
|
39
|
-
CATALAN = "003"
|
|
40
|
-
FRENCH = "004"
|
|
41
|
-
GERMAN = "005"
|
|
42
|
-
DUTCH = "006"
|
|
43
|
-
ITALIAN = "007"
|
|
44
|
-
SWEDISH = "008"
|
|
45
|
-
PORTUGUESE = "009"
|
|
46
|
-
VALENCIAN = "010"
|
|
47
|
-
POLISH = "011"
|
|
48
|
-
GALICIAN = "012"
|
|
49
|
-
EUSKERA = "013"
|
|
50
|
-
|
|
51
|
-
LANGUAGES = [
|
|
52
|
-
CUSTOMER,
|
|
53
|
-
SPANISH,
|
|
54
|
-
ENGLISH,
|
|
55
|
-
CATALAN,
|
|
56
|
-
FRENCH,
|
|
57
|
-
GERMAN,
|
|
58
|
-
DUTCH,
|
|
59
|
-
ITALIAN,
|
|
60
|
-
SWEDISH,
|
|
61
|
-
PORTUGUESE,
|
|
62
|
-
VALENCIAN,
|
|
63
|
-
POLISH,
|
|
64
|
-
GALICIAN,
|
|
65
|
-
EUSKERA,
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
# Transaction types
|
|
69
|
-
""" Standard payment """
|
|
70
|
-
STANDARD_PAYMENT = "0"
|
|
71
|
-
""" Pre-authorization """
|
|
72
|
-
PREAUTHORIZATION = "1"
|
|
73
|
-
""" Confirmation of pre-authorization """
|
|
74
|
-
PREAUTHORIZATION_CONFIRMATION = "2"
|
|
75
|
-
""" Partial or total refund """
|
|
76
|
-
REFUND = "3"
|
|
77
|
-
""" Recurring transaction """
|
|
78
|
-
RECURRING_TRANSACTION = "5"
|
|
79
|
-
""" Successive transaction """
|
|
80
|
-
SUCCESSIVE_TRANSACTION = "6"
|
|
81
|
-
""" Authentication """
|
|
82
|
-
AUTHENTICATION = "7"
|
|
83
|
-
""" Confirmation of authentication """
|
|
84
|
-
AUTHENTICATION_CONFIRMATION = "8"
|
|
85
|
-
""" Cancellation of pre-authorization """
|
|
86
|
-
PREAUTHORIZATION_CANCELATION = "9"
|
|
87
|
-
""" Deferred pre-authorization """
|
|
88
|
-
DEFERRED_PREAUTHORIZATION = "O"
|
|
89
|
-
""" Confirmation of deferred pre-authorization """
|
|
90
|
-
DEFERRED_PREAUTHORIZATION_CONFIRMATION = "P"
|
|
91
|
-
""" Cancelation of deferred pre-authorization """
|
|
92
|
-
DEFERRED_PREAUTHORIZATION_CANCELATION = "Q"
|
|
93
|
-
""" Recurring deferred pre-authorization """
|
|
94
|
-
RECURRING_DEFERRED_PREAUTHORIZATION = "R"
|
|
95
|
-
""" Confirmation of recurring deferred pre-authorization and successive transaction """
|
|
96
|
-
SUCCESSIVE_RECURRING_TRANSACTION = "S"
|
|
97
|
-
|
|
98
|
-
TRANSACTIONS = [
|
|
99
|
-
STANDARD_PAYMENT,
|
|
100
|
-
PREAUTHORIZATION,
|
|
101
|
-
PREAUTHORIZATION_CONFIRMATION,
|
|
102
|
-
REFUND,
|
|
103
|
-
RECURRING_TRANSACTION,
|
|
104
|
-
SUCCESSIVE_TRANSACTION,
|
|
105
|
-
AUTHENTICATION,
|
|
106
|
-
AUTHENTICATION_CONFIRMATION,
|
|
107
|
-
PREAUTHORIZATION_CANCELATION,
|
|
108
|
-
DEFERRED_PREAUTHORIZATION,
|
|
109
|
-
DEFERRED_PREAUTHORIZATION_CONFIRMATION,
|
|
110
|
-
DEFERRED_PREAUTHORIZATION_CANCELATION,
|
|
111
|
-
RECURRING_DEFERRED_PREAUTHORIZATION,
|
|
112
|
-
SUCCESSIVE_RECURRING_TRANSACTION,
|
|
113
|
-
]
|
redsys/response.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
from decimal import Decimal
|
|
2
|
-
from typing import Any
|
|
3
|
-
from typing import Dict
|
|
4
|
-
|
|
5
|
-
RESPONSE = "Ds_Response"
|
|
6
|
-
DATE = "Ds_Date"
|
|
7
|
-
HOUR = "Ds_Hour"
|
|
8
|
-
MERCHANT_CODE = "Ds_MerchantCode"
|
|
9
|
-
TERMINAL = "Ds_Terminal"
|
|
10
|
-
TRANSACTION_TYPE = "Ds_TransactionType"
|
|
11
|
-
ORDER = "Ds_Order"
|
|
12
|
-
CURRENCY = "Ds_Currency"
|
|
13
|
-
AMOUNT = "Ds_Amount"
|
|
14
|
-
MERCHANT_DATA = "Ds_MerchantData"
|
|
15
|
-
CONSUMER_LANGUAGE = "Ds_ConsumerLanguage"
|
|
16
|
-
CARD_COUNTRY = "Ds_Card_Country"
|
|
17
|
-
CARD_TYPE = "Ds_Card_Type"
|
|
18
|
-
CARD_BRAND = "Ds_Card_Brand"
|
|
19
|
-
MERCHANT_COF_TXNID = "Ds_Merchant_Cof_Txnid"
|
|
20
|
-
SECURE_PAYMENT = "Ds_SecurePayment"
|
|
21
|
-
AUTHORIZATION_CODE = "Ds_AuthorisationCode"
|
|
22
|
-
ERROR_CODE = "Ds_ErrorCode"
|
|
23
|
-
AMOUNT_EURO = "Ds_Amount_Euro"
|
|
24
|
-
PROCESSED_PAY_METHOD = "Ds_ProcessedPayMethod"
|
|
25
|
-
|
|
26
|
-
RESPONSE_MAP = {
|
|
27
|
-
"0000": "Transacción autorizada para pagos y preautorizaciones",
|
|
28
|
-
"900": "Transacción autorizada para devoluciones y confirmaciones",
|
|
29
|
-
"400": "Transacción autorizada para anulaciones",
|
|
30
|
-
"101": "Tarjeta caducada",
|
|
31
|
-
"102": "Tarjeta en excepción transitoria o bajo sospecha de fraude",
|
|
32
|
-
"106": "Intentos de PIN excedidos",
|
|
33
|
-
"125": "Tarjeta no efectiva",
|
|
34
|
-
"129": "Código de seguridad (CVV2/CVC2) incorrecto",
|
|
35
|
-
"180": "Tarjeta ajena al servicio",
|
|
36
|
-
"184": "Error en la autenticación del titular",
|
|
37
|
-
"190": "Denegación del emisor sin especificar motivo",
|
|
38
|
-
"191": "Fecha de caducidad errónea",
|
|
39
|
-
"202": "Tarjeta en excepción transitoria o bajo sospecha de fraude con retirada de tarjeta",
|
|
40
|
-
"904": "Comercio no registrado en FUC",
|
|
41
|
-
"909": "Error de sistema",
|
|
42
|
-
"912": "Emisor no disponible",
|
|
43
|
-
"913": "Pedido repetido",
|
|
44
|
-
"944": "Sesión Incorrecta",
|
|
45
|
-
"950": "Operación de devolución no permitida",
|
|
46
|
-
"9912": "Emisor no disponible",
|
|
47
|
-
"9064": "Número de posiciones de la tarjeta incorrecto",
|
|
48
|
-
"9078": "Tipo de operación no permitida para esa tarjeta",
|
|
49
|
-
"9093": "Tarjeta no existente",
|
|
50
|
-
"9094": "Rechazo servidores internacionales",
|
|
51
|
-
"9104": "Comercio con “titular seguro” y titular sin clave de compra segura",
|
|
52
|
-
"9218": "El comercio no permite op. seguras por entrada /operaciones",
|
|
53
|
-
"9253": "Tarjeta no cumple el check-digit",
|
|
54
|
-
"9256": "El comercio no puede realizar preautorizaciones",
|
|
55
|
-
"9257": "Esta tarjeta no permite operativa de preautorizaciones",
|
|
56
|
-
"9261": "Operación detenida por superar el control de restricciones en la entrada al SIS",
|
|
57
|
-
"9913": "Error en la confirmación que el comercio envía al TPV Virtual",
|
|
58
|
-
"9914": "Confirmación “KO” del comercio",
|
|
59
|
-
"9915": "A petición del usuario se ha cancelado el pago",
|
|
60
|
-
"9928": "Anulación de autorización en diferido realizada por el SIS (proceso batch)",
|
|
61
|
-
"9929": "Anulación de autorización en diferido realizada por el comercio",
|
|
62
|
-
"9997": "Se está procesando otra transacción en SIS con la misma tarjeta",
|
|
63
|
-
"9998": "Operación en proceso de solicitud de datos de tarjeta",
|
|
64
|
-
"9999": "Operación que ha sido redirigida al emisor a autenticar",
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
MERCHANT_PARAMETERS_MAP = {
|
|
68
|
-
"response": RESPONSE,
|
|
69
|
-
"date": DATE,
|
|
70
|
-
"hour": HOUR,
|
|
71
|
-
"merchant_code": MERCHANT_CODE,
|
|
72
|
-
"terminal": TERMINAL,
|
|
73
|
-
"transaction_type": TRANSACTION_TYPE,
|
|
74
|
-
"order": ORDER,
|
|
75
|
-
"currency": CURRENCY,
|
|
76
|
-
"amount": AMOUNT,
|
|
77
|
-
"merchant_data": MERCHANT_DATA,
|
|
78
|
-
"consumer_language": CONSUMER_LANGUAGE,
|
|
79
|
-
"card_country": CARD_COUNTRY,
|
|
80
|
-
"card_type": CARD_TYPE,
|
|
81
|
-
"card_brand": CARD_BRAND,
|
|
82
|
-
"merchant_cof_txnid": MERCHANT_COF_TXNID,
|
|
83
|
-
"secure_payment": SECURE_PAYMENT,
|
|
84
|
-
"authorization_code": AUTHORIZATION_CODE,
|
|
85
|
-
"error_code": ERROR_CODE,
|
|
86
|
-
"amount_euro": AMOUNT_EURO,
|
|
87
|
-
"processed_pay_method": PROCESSED_PAY_METHOD,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class Response:
|
|
92
|
-
"""
|
|
93
|
-
Defines a response
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
_parameters: Dict[str, Any]
|
|
97
|
-
|
|
98
|
-
def __init__(self, parameters: Dict[str, Any]):
|
|
99
|
-
# __setattr__ only handles merchant parameters, so the per-instance store is set directly.
|
|
100
|
-
object.__setattr__(self, "_parameters", {})
|
|
101
|
-
MERCHANT_PARAMETERS_MAP_REVERSE = {value: key for key, value in MERCHANT_PARAMETERS_MAP.items()}
|
|
102
|
-
for key, value in parameters.items():
|
|
103
|
-
reversed_parameter = MERCHANT_PARAMETERS_MAP_REVERSE.get(key, key)
|
|
104
|
-
clean = getattr(self, "clean_%s" % reversed_parameter, None)
|
|
105
|
-
self._parameters[reversed_parameter] = clean(value) if clean else value
|
|
106
|
-
|
|
107
|
-
def __getattr__(self, item: str) -> Any:
|
|
108
|
-
if item in MERCHANT_PARAMETERS_MAP:
|
|
109
|
-
return self._parameters.get(item)
|
|
110
|
-
|
|
111
|
-
def __setattr__(self, key: str, value: Any):
|
|
112
|
-
if key in MERCHANT_PARAMETERS_MAP:
|
|
113
|
-
clean = getattr(self, "clean_%s" % MERCHANT_PARAMETERS_MAP[key], None)
|
|
114
|
-
self._parameters[MERCHANT_PARAMETERS_MAP[key]] = clean(value) if clean else value
|
|
115
|
-
|
|
116
|
-
@property
|
|
117
|
-
def is_authorized(self):
|
|
118
|
-
return (0 <= self.code <= 99) or self.code == 900 or self.code == 400
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
def is_paid(self):
|
|
122
|
-
return 0 <= self.code <= 99
|
|
123
|
-
|
|
124
|
-
@property
|
|
125
|
-
def is_refunded(self):
|
|
126
|
-
return self.code == 900
|
|
127
|
-
|
|
128
|
-
@property
|
|
129
|
-
def is_canceled(self):
|
|
130
|
-
return self.code == 400
|
|
131
|
-
|
|
132
|
-
@property
|
|
133
|
-
def code(self):
|
|
134
|
-
return int(self.response)
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def message(self):
|
|
138
|
-
if self.is_paid:
|
|
139
|
-
return RESPONSE_MAP["0000"]
|
|
140
|
-
# RedSys sends 4-digit codes ("0190"); the map keys drop the leading zeros ("190").
|
|
141
|
-
return RESPONSE_MAP.get(self.response) or RESPONSE_MAP.get(str(self.code))
|
|
142
|
-
|
|
143
|
-
@property
|
|
144
|
-
def parameters(self):
|
|
145
|
-
return self._parameters
|
|
146
|
-
|
|
147
|
-
@staticmethod
|
|
148
|
-
def clean_amount(value):
|
|
149
|
-
return Decimal(f"{str(value)[:-2]}.{str(value)[-2:]}")
|
redsys/tests/__init__.py
DELETED
|
File without changes
|
redsys/tests/test_client.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from decimal import ROUND_HALF_UP
|
|
2
|
-
from decimal import Decimal as D
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from redsys.client import RedirectClient
|
|
7
|
-
from redsys.constants import EUR
|
|
8
|
-
from redsys.constants import STANDARD_PAYMENT
|
|
9
|
-
from redsys.request import Request
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestRedirectClient:
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def set_up(self):
|
|
15
|
-
secret_key = "sq7HjrUOBfKmC576ILgskD5srU870gJ7"
|
|
16
|
-
self.client = RedirectClient(secret_key)
|
|
17
|
-
self.parameters = {
|
|
18
|
-
"merchant_code": "100000001",
|
|
19
|
-
"terminal": "1",
|
|
20
|
-
"transaction_type": STANDARD_PAYMENT,
|
|
21
|
-
"currency": EUR,
|
|
22
|
-
"order": "000000000001",
|
|
23
|
-
"amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
|
|
24
|
-
"merchant_data": "test merchant data",
|
|
25
|
-
"merchant_name": "Example Commerce",
|
|
26
|
-
"titular": "Example Ltd.",
|
|
27
|
-
"product_description": "Products of Example Commerce",
|
|
28
|
-
"merchant_url": "https://example.com/redsys/response",
|
|
29
|
-
}
|
|
30
|
-
self.request = Request(self.parameters)
|
|
31
|
-
self.merchant_params = self.request.prepare_parameters()
|
|
32
|
-
|
|
33
|
-
def test_encode_parameters(self):
|
|
34
|
-
encoded_params = self.client.encode_parameters(self.merchant_params)
|
|
35
|
-
encoded_params_str = encoded_params.decode("UTF-8")
|
|
36
|
-
assert encoded_params_str.endswith("ZWRzeXMvcmVzcG9uc2UifQ==")
|
|
37
|
-
assert len(encoded_params_str) == 628
|
|
38
|
-
|
|
39
|
-
def test_decode_parameters(self):
|
|
40
|
-
encoded_params = self.client.encode_parameters(self.merchant_params)
|
|
41
|
-
decoded_params = self.client.decode_parameters(encoded_params)
|
|
42
|
-
assert decoded_params == self.merchant_params
|
|
43
|
-
|
|
44
|
-
def test_encrypt_3DES(self):
|
|
45
|
-
encrypted_order = self.client.encrypt_3DES(self.request.order)
|
|
46
|
-
assert len(encrypted_order) == 16
|
|
47
|
-
assert str(encrypted_order) == "b'/\\x17\\\\6\\n\\x1aY\\xf3\\xd7\\xa5\\x97\\xb9\\xb6\\xe0\\x8dH'"
|
|
48
|
-
|
|
49
|
-
def test_sign_hmac256(self):
|
|
50
|
-
encrypted_order = self.client.encrypt_3DES(self.request.order)
|
|
51
|
-
encoded_params = self.client.encode_parameters(self.merchant_params)
|
|
52
|
-
signature = self.client.sign_hmac256(encrypted_order, encoded_params)
|
|
53
|
-
assert signature == b"a2F05DwuH5aXu5vD1IdjQ+NxaR0lPsRwksW1nr2Nzuw="
|
|
54
|
-
|
|
55
|
-
def test_prepare_request(self):
|
|
56
|
-
prepared_request = self.client.prepare_request(self.parameters)
|
|
57
|
-
assert len(prepared_request.keys()) == 3
|
|
58
|
-
assert prepared_request.get("Ds_Signature") == b"a2F05DwuH5aXu5vD1IdjQ+NxaR0lPsRwksW1nr2Nzuw="
|
|
59
|
-
pass
|
|
60
|
-
|
|
61
|
-
def test_create_response_valid(self):
|
|
62
|
-
merchant_parameters = {
|
|
63
|
-
"Ds_MerchantCode": "100000001",
|
|
64
|
-
"Ds_Terminal": "1",
|
|
65
|
-
"Ds_TransactionType": "0",
|
|
66
|
-
"Ds_Currency": 978,
|
|
67
|
-
"Ds_Order": "000000000001",
|
|
68
|
-
"Ds_Amount": 1056,
|
|
69
|
-
"Ds_MerchantData": "test merchant data",
|
|
70
|
-
}
|
|
71
|
-
encoded_params = self.client.encode_parameters(merchant_parameters)
|
|
72
|
-
signature = self.client.generate_signature(self.request.order, encoded_params)
|
|
73
|
-
response = self.client.create_response(signature.decode(), encoded_params.decode())
|
|
74
|
-
assert response._parameters.get("order") == merchant_parameters.get("Ds_Order")
|
|
75
|
-
|
|
76
|
-
def test_create_response_invalid(self):
|
|
77
|
-
merchant_parameters = {
|
|
78
|
-
"Ds_MerchantCode": "100000001",
|
|
79
|
-
"Ds_Terminal": "1",
|
|
80
|
-
"Ds_TransactionType": "0",
|
|
81
|
-
"Ds_Currency": 978,
|
|
82
|
-
"Ds_Order": "000000000001",
|
|
83
|
-
"Ds_Amount": 1056,
|
|
84
|
-
"Ds_MerchantData": "test merchant data",
|
|
85
|
-
}
|
|
86
|
-
encoded_params = self.client.encode_parameters(merchant_parameters)
|
|
87
|
-
bad_signature = b"sadfe2r3q2fdssaf3"
|
|
88
|
-
with pytest.raises(ValueError, match="The provided signature is not valid"):
|
|
89
|
-
self.client.create_response(bad_signature.decode(), encoded_params.decode())
|