python-redsys 1.2.0__py3-none-any.whl → 1.2.2__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.
@@ -0,0 +1,122 @@
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
+ [![CI](https://github.com/ddiazpinto/python-redsys/actions/workflows/python-app.yml/badge.svg?branch=release/1.x)](https://github.com/ddiazpinto/python-redsys/actions/workflows/python-app.yml?query=branch%3Arelease%2F1.x) [![PyPI](https://img.shields.io/pypi/v/python-redsys.svg)](https://pypi.org/project/python-redsys/) ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+
@@ -0,0 +1,13 @@
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.7.0
2
+ Generator: poetry-core 2.5.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
redsys/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.2.0"
1
+ __version__ = "1.2.2"
2
2
  __version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split("."))
redsys/request.py CHANGED
@@ -77,9 +77,11 @@ class Request:
77
77
  their values according to the platform specifications
78
78
  """
79
79
 
80
- _parameters: Dict[str, Any] = {}
80
+ _parameters: Dict[str, Any]
81
81
 
82
82
  def __init__(self, parameters: Dict[str, Any]) -> None:
83
+ # __setattr__ only handles merchant parameters, so the per-instance store is set directly.
84
+ object.__setattr__(self, "_parameters", {})
83
85
  for key, value in parameters.items():
84
86
  if key in MERCHANT_PARAMETERS_MAP:
85
87
  if check := getattr(self, f"check_{str(key)}", None):
@@ -90,7 +92,7 @@ class Request:
90
92
 
91
93
  def __getattr__(self, item: str) -> Any:
92
94
  if item in MERCHANT_PARAMETERS_MAP:
93
- return self._parameters[item]
95
+ return self._parameters.get(item)
94
96
 
95
97
  def __setattr__(self, key, value):
96
98
  if key in MERCHANT_PARAMETERS_MAP:
redsys/response.py CHANGED
@@ -93,9 +93,11 @@ class Response:
93
93
  Defines a response
94
94
  """
95
95
 
96
- _parameters: Dict[str, Any] = {}
96
+ _parameters: Dict[str, Any]
97
97
 
98
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", {})
99
101
  MERCHANT_PARAMETERS_MAP_REVERSE = {value: key for key, value in MERCHANT_PARAMETERS_MAP.items()}
100
102
  for key, value in parameters.items():
101
103
  reversed_parameter = MERCHANT_PARAMETERS_MAP_REVERSE.get(key, key)
@@ -104,7 +106,7 @@ class Response:
104
106
 
105
107
  def __getattr__(self, item: str) -> Any:
106
108
  if item in MERCHANT_PARAMETERS_MAP:
107
- return self._parameters[item]
109
+ return self._parameters.get(item)
108
110
 
109
111
  def __setattr__(self, key: str, value: Any):
110
112
  if key in MERCHANT_PARAMETERS_MAP:
@@ -133,7 +135,10 @@ class Response:
133
135
 
134
136
  @property
135
137
  def message(self):
136
- return RESPONSE_MAP["0000"] if self.is_paid else RESPONSE_MAP[self.response]
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))
137
142
 
138
143
  @property
139
144
  def parameters(self):
@@ -63,3 +63,12 @@ class TestRequest:
63
63
  merchant_url = "".join(choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(251))
64
64
  with pytest.raises(ValueError):
65
65
  assert Request.check_merchant_url(merchant_url)
66
+
67
+ def test_requests_do_not_share_parameters(self):
68
+ first = Request(
69
+ {"order": "000000000001", "amount": D("1.00"), "currency": EUR, "mobile_number": "+34700000000"}
70
+ )
71
+ second = Request({"order": "000000000002", "amount": D("2.00"), "currency": EUR})
72
+ assert "Ds_Merchant_Bizum_MobileNumber" not in second.prepare_parameters()
73
+ assert second.mobile_number is None
74
+ assert first.prepare_parameters()["Ds_Merchant_Bizum_MobileNumber"] == "+34700000000"
@@ -1,3 +1,5 @@
1
+ from decimal import Decimal
2
+
1
3
  from redsys.response import Response
2
4
 
3
5
 
@@ -21,3 +23,17 @@ class TestResponse:
21
23
  assert response.is_paid is True
22
24
  assert response.is_canceled is False
23
25
  assert response.is_refunded is False
26
+
27
+ def test_responses_do_not_share_parameters(self):
28
+ first = Response({"Ds_Response": "0000", "Ds_Order": "000000000001", "Ds_Amount": "100"})
29
+ second = Response({"Ds_Response": "0190", "Ds_Order": "000000000002"})
30
+ assert "amount" not in second.parameters
31
+ assert second.amount is None
32
+ assert first.amount == Decimal("1.00")
33
+
34
+ def test_message_for_four_digit_codes(self):
35
+ assert Response({"Ds_Response": "0190"}).message == "Denegación del emisor sin especificar motivo"
36
+ assert Response({"Ds_Response": "0900"}).message == "Transacción autorizada para devoluciones y confirmaciones"
37
+ assert Response({"Ds_Response": "0400"}).message == "Transacción autorizada para anulaciones"
38
+ assert Response({"Ds_Response": "9915"}).message == "A petición del usuario se ha cancelado el pago"
39
+ assert Response({"Ds_Response": "0105"}).message is None
@@ -1,135 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-redsys
3
- Version: 1.2.0
4
- Summary: A simple, clean and less dependant client to handle payments through Redsys.
5
- Home-page: https://github.com/systemallica/python-redsys
6
- License: MIT
7
- Author: Andrés Reverón Molina
8
- Author-email: andres@reveronmolina.me
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
- Requires-Dist: pycryptodome (>=3.19.0,<4.0.0)
19
- Description-Content-Type: text/markdown
20
-
21
-
22
- [![PyPI version](https://badge.fury.io/py/python-redsys.svg)](https://badge.fury.io/py/python-redsys)
23
-
24
- # Welcome to python-redsys!
25
-
26
- A simple, clean and less dependant client to handle payments through the
27
- Redsys platform using one of the available methods: _redirect connection_ or (secure method).
28
-
29
- The purpose of this library is to provide a normalized interface between
30
- Redsys and other Python applications.
31
-
32
- **About RedirectClient**
33
-
34
- Although _redirect connection_ depends on a webserver to resolve the
35
- communication step, the RedirectClient provided in this library does not
36
- assume any kind of procedure to resolve that step; it merely prepares
37
- the necessary parameters to make a request and handles the corresponding
38
- response parameters. That's what less dependant means.
39
-
40
- ## Example using _redirect connection_
41
-
42
- ### 0. Install python-redsys
43
-
44
- You can add python-redsys to your project with pip:
45
- > pip install python-redsys
46
-
47
- Or with poetry:
48
- > poetry add python-redsys
49
-
50
- ### 1. Instantiate the redirect client
51
-
52
- ```python
53
- from decimal import Decimal as D, ROUND_HALF_UP
54
- from redsys.constants import EUR, STANDARD_PAYMENT
55
- from redsys.client import RedirectClient
56
-
57
- secret_key = "123456789abcdef"
58
- client = RedirectClient(secret_key)
59
- ```
60
-
61
- ### 2. Set up the request parameters
62
-
63
- ```python
64
- parameters = {
65
- "merchant_code": "100000001",
66
- "terminal": "1",
67
- "transaction_type": STANDARD_PAYMENT,
68
- "currency": EUR,
69
- "order": "000000001",
70
- "amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
71
- "merchant_data": "test merchant data",
72
- "merchant_name": "Example Commerce",
73
- "titular": "Example Ltd.",
74
- "product_description": "Products of Example Commerce",
75
- "merchant_url": "https://example.com/redsys/response",
76
- }
77
- ```
78
-
79
- ### 3. Prepare the request
80
-
81
- This method returns a dict with the necessary post parameters that are
82
- needed during the communication step.
83
-
84
- ```python
85
- args = client.prepare_request(parameters)
86
- ```
87
-
88
- ### 4. Communication step
89
-
90
- Redirect the _user-agent_ to the corresponding Redsys' endpoint using
91
- the post parameters given in the previous step.
92
-
93
- After the payment process is finished, Redsys will respond making a
94
- request to the `merchant_url` defined in step 2.
95
-
96
- ### 5. Create and check the response
97
-
98
- Create the response object using the received parameters from Redsys.
99
- The method `create_response()` throws a `ValueError` in case the
100
- received signature is not equal to the calculated one using the
101
- given `merchant_parameters`. This normally means that the response **is
102
- not coming from Redsys** or that it **has been compromised**.
103
-
104
- ```python
105
- signature = "YqFenHc2HpB273l8c995...."
106
- merchant_parameters = "AndvIh66VZdkC5TG3nYL5j4XfCnFFbo3VkOu9TAeTs58fxddgc..."
107
- response = client.create_response(signature, merchant_parameters)
108
- if response.is_paid:
109
- # Do the corresponding actions after a successful payment
110
- else:
111
- # Do the corresponding actions after a failed payment
112
- raise Exception(response.code, response.message)
113
- ```
114
-
115
- **Methods for checking the response:**
116
-
117
- According to the Redsys documentation:
118
-
119
- - `response.is_paid`: Returns `True` if the response code is
120
- between 0 and 99 (both included).
121
- - `response.is_canceled`: Returns `True` if the response code
122
- is 400.
123
- - `response.is_refunded`: Returns `True` if the response code
124
- is 900.
125
- - `response.is_authorized`: Returns `True` if the response is
126
- **paid**, **refunded** or **canceled**.
127
-
128
- Also, you can directly access the code or the message defined in Redsys
129
- documentation using `response.code` or `response.message`.
130
-
131
- ## Contributions
132
-
133
- Please, feel free to send any contribution that maintains the _less
134
- dependant_ philosophy.
135
-
@@ -1,13 +0,0 @@
1
- redsys/__init__.py,sha256=mBK19nPR_NOqZ1AHA73PWWuxqU4gDY1nssPXujFjOo0,138
2
- redsys/client.py,sha256=ervLX4FbzDfWjh_SO2SFyY9IT0PZ5aIcVZdYy4mUO7Q,3937
3
- redsys/constants.py,sha256=lJYgbmikxh0b9iVM4M5XFWBWVvdFY27OkcXSLluHSsQ,2497
4
- redsys/request.py,sha256=t9zStQ4QcRfMA7xDU-Qt6zKAyUA5bzvmD0aoCjjHgi4,5154
5
- redsys/response.py,sha256=qJaeuCUfzGc3dBRS_yzBWGJ6yWtZLj0P4ioOGsr5e8Y,5280
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=QRD-m7O9AVokpu2SgcirXqMU17GDn0tqzFyjkeneHHE,2601
9
- redsys/tests/test_response.py,sha256=Q7rQ6TXUWBnIoNlqTWAwzTFq5gv_naEYZ1r8RYid18c,826
10
- python_redsys-1.2.0.dist-info/LICENSE,sha256=Ku6mJcUOcHd2aZ4o2Jh4Tjkx1oZT-iT0VCyUNvm2IXU,1257
11
- python_redsys-1.2.0.dist-info/METADATA,sha256=tYputxvq5_laNsEPPSs-Y5A3gBjzhzzqljryn2KOdb4,4413
12
- python_redsys-1.2.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
13
- python_redsys-1.2.0.dist-info/RECORD,,