python-redsys 1.2.1__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.1.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.1.dist-info/METADATA +0 -140
- python_redsys-1.2.1.dist-info/RECORD +0 -13
- python_redsys-1.2.1.dist-info/licenses/LICENSE +0 -25
- redsys/client.py +0 -105
- redsys/constants.py +0 -113
- redsys/response.py +0 -146
- 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 -32
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-redsys
|
|
3
|
-
Version: 1.2.1
|
|
4
|
-
Summary: A simple, clean and less dependant client to handle payments through Redsys.
|
|
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.0,<4.0.0)
|
|
23
|
-
Project-URL: Homepage, https://github.com/ddiazpinto/python-redsys
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
[](https://badge.fury.io/py/python-redsys)
|
|
28
|
-
|
|
29
|
-
# Welcome to python-redsys!
|
|
30
|
-
|
|
31
|
-
A simple, clean and less dependant client to handle payments through the
|
|
32
|
-
Redsys platform using one of the available methods: _redirect connection_ or (secure method).
|
|
33
|
-
|
|
34
|
-
The purpose of this library is to provide a normalized interface between
|
|
35
|
-
Redsys and other Python applications.
|
|
36
|
-
|
|
37
|
-
**About RedirectClient**
|
|
38
|
-
|
|
39
|
-
Although _redirect connection_ depends on a webserver to resolve the
|
|
40
|
-
communication step, the RedirectClient provided in this library does not
|
|
41
|
-
assume any kind of procedure to resolve that step; it merely prepares
|
|
42
|
-
the necessary parameters to make a request and handles the corresponding
|
|
43
|
-
response parameters. That's what less dependant means.
|
|
44
|
-
|
|
45
|
-
## Example using _redirect connection_
|
|
46
|
-
|
|
47
|
-
### 0. Install python-redsys
|
|
48
|
-
|
|
49
|
-
You can add python-redsys to your project with pip:
|
|
50
|
-
> pip install python-redsys
|
|
51
|
-
|
|
52
|
-
Or with poetry:
|
|
53
|
-
> poetry add python-redsys
|
|
54
|
-
|
|
55
|
-
### 1. Instantiate the redirect client
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
from decimal import Decimal as D, ROUND_HALF_UP
|
|
59
|
-
from redsys.constants import EUR, STANDARD_PAYMENT
|
|
60
|
-
from redsys.client import RedirectClient
|
|
61
|
-
|
|
62
|
-
secret_key = "123456789abcdef"
|
|
63
|
-
client = RedirectClient(secret_key)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 2. Set up the request parameters
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
parameters = {
|
|
70
|
-
"merchant_code": "100000001",
|
|
71
|
-
"terminal": "1",
|
|
72
|
-
"transaction_type": STANDARD_PAYMENT,
|
|
73
|
-
"currency": EUR,
|
|
74
|
-
"order": "000000001",
|
|
75
|
-
"amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
|
|
76
|
-
"merchant_data": "test merchant data",
|
|
77
|
-
"merchant_name": "Example Commerce",
|
|
78
|
-
"titular": "Example Ltd.",
|
|
79
|
-
"product_description": "Products of Example Commerce",
|
|
80
|
-
"merchant_url": "https://example.com/redsys/response",
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### 3. Prepare the request
|
|
85
|
-
|
|
86
|
-
This method returns a dict with the necessary post parameters that are
|
|
87
|
-
needed during the communication step.
|
|
88
|
-
|
|
89
|
-
```python
|
|
90
|
-
args = client.prepare_request(parameters)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### 4. Communication step
|
|
94
|
-
|
|
95
|
-
Redirect the _user-agent_ to the corresponding Redsys' endpoint using
|
|
96
|
-
the post parameters given in the previous step.
|
|
97
|
-
|
|
98
|
-
After the payment process is finished, Redsys will respond making a
|
|
99
|
-
request to the `merchant_url` defined in step 2.
|
|
100
|
-
|
|
101
|
-
### 5. Create and check the response
|
|
102
|
-
|
|
103
|
-
Create the response object using the received parameters from Redsys.
|
|
104
|
-
The method `create_response()` throws a `ValueError` in case the
|
|
105
|
-
received signature is not equal to the calculated one using the
|
|
106
|
-
given `merchant_parameters`. This normally means that the response **is
|
|
107
|
-
not coming from Redsys** or that it **has been compromised**.
|
|
108
|
-
|
|
109
|
-
```python
|
|
110
|
-
signature = "YqFenHc2HpB273l8c995...."
|
|
111
|
-
merchant_parameters = "AndvIh66VZdkC5TG3nYL5j4XfCnFFbo3VkOu9TAeTs58fxddgc..."
|
|
112
|
-
response = client.create_response(signature, merchant_parameters)
|
|
113
|
-
if response.is_paid:
|
|
114
|
-
# Do the corresponding actions after a successful payment
|
|
115
|
-
else:
|
|
116
|
-
# Do the corresponding actions after a failed payment
|
|
117
|
-
raise Exception(response.code, response.message)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Methods for checking the response:**
|
|
121
|
-
|
|
122
|
-
According to the Redsys documentation:
|
|
123
|
-
|
|
124
|
-
- `response.is_paid`: Returns `True` if the response code is
|
|
125
|
-
between 0 and 99 (both included).
|
|
126
|
-
- `response.is_canceled`: Returns `True` if the response code
|
|
127
|
-
is 400.
|
|
128
|
-
- `response.is_refunded`: Returns `True` if the response code
|
|
129
|
-
is 900.
|
|
130
|
-
- `response.is_authorized`: Returns `True` if the response is
|
|
131
|
-
**paid**, **refunded** or **canceled**.
|
|
132
|
-
|
|
133
|
-
Also, you can directly access the code or the message defined in Redsys
|
|
134
|
-
documentation using `response.code` or `response.message`.
|
|
135
|
-
|
|
136
|
-
## Contributions
|
|
137
|
-
|
|
138
|
-
Please, feel free to send any contribution that maintains the _less
|
|
139
|
-
dependant_ philosophy.
|
|
140
|
-
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
redsys/__init__.py,sha256=kRuS8epm885TJGvdrjNc9xwT70glq_h9x71_bUgT68s,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=leDhlDlnVLGeruxlN2_3DQ-V56xAbIiFkvoqYDmz9S0,5430
|
|
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=V8WdZ1oR4Ed5ZKmi2R-Hpgnr3AyYZy-jFH1yn2S0lA4,1220
|
|
10
|
-
python_redsys-1.2.1.dist-info/METADATA,sha256=Wn6Z7SG3HtRu0PIaxBwEzsDomMFzxO_BH9M4NpFu-uA,4631
|
|
11
|
-
python_redsys-1.2.1.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
12
|
-
python_redsys-1.2.1.dist-info/licenses/LICENSE,sha256=Ku6mJcUOcHd2aZ4o2Jh4Tjkx1oZT-iT0VCyUNvm2IXU,1257
|
|
13
|
-
python_redsys-1.2.1.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,146 +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
|
-
return RESPONSE_MAP["0000"] if self.is_paid else RESPONSE_MAP[self.response]
|
|
139
|
-
|
|
140
|
-
@property
|
|
141
|
-
def parameters(self):
|
|
142
|
-
return self._parameters
|
|
143
|
-
|
|
144
|
-
@staticmethod
|
|
145
|
-
def clean_amount(value):
|
|
146
|
-
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())
|