python-redsys 1.1.1__py3-none-any.whl → 1.2.1__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.
@@ -1,21 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-redsys
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: A simple, clean and less dependant client to handle payments through Redsys.
5
- Home-page: https://github.com/systemallica/python-redsys
6
5
  License: MIT
7
- Author: Andrés Reverón Molina
8
- Author-email: andres@reveronmolina.me
9
- Requires-Python: >=3.7,<4.0
6
+ License-File: LICENSE
7
+ Author: David Díaz
8
+ Author-email: d.diazp@gmail.com
9
+ Requires-Python: >=3.8,<4.0
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.7
16
14
  Classifier: Programming Language :: Python :: 3.8
17
15
  Classifier: Programming Language :: Python :: 3.9
18
- Requires-Dist: pycryptodome (>=3.12.0,<4.0.0)
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
19
24
  Description-Content-Type: text/markdown
20
25
 
21
26
 
@@ -0,0 +1,13 @@
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry 1.0.7
2
+ Generator: poetry-core 2.5.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
redsys/__init__.py CHANGED
@@ -1,5 +1,2 @@
1
- __version__ = "1.1.1"
2
- __version_info__ = tuple(
3
- int(num) if num.isdigit() else num
4
- for num in __version__.replace("-", ".", 1).split(".")
5
- )
1
+ __version__ = "1.2.1"
2
+ __version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split("."))
redsys/client.py CHANGED
@@ -3,8 +3,10 @@ import hashlib
3
3
  import hmac
4
4
  import json
5
5
  import re
6
- from abc import ABC, abstractmethod
7
- from typing import Any, Dict
6
+ from abc import ABC
7
+ from abc import abstractmethod
8
+ from typing import Any
9
+ from typing import Dict
8
10
 
9
11
  from Crypto.Cipher import DES3
10
12
 
@@ -51,16 +53,12 @@ class Client(ABC):
51
53
  Generates the encrypted signature using the 3DES-encrypted order
52
54
  and base64-encoded merchant parameters
53
55
  """
54
- signature = hmac.new(
55
- encrypted_order, merchant_parameters, hashlib.sha256
56
- ).digest()
56
+ signature = hmac.new(encrypted_order, merchant_parameters, hashlib.sha256).digest()
57
57
  return base64.b64encode(signature)
58
58
 
59
59
  def encrypt_3DES(self, order: str) -> bytes:
60
60
  """Encrypts(3DES algorithm) the payment order using the secret key"""
61
- cipher = DES3.new(
62
- base64.b64decode(self.secret_key), DES3.MODE_CBC, IV=b"\0\0\0\0\0\0\0\0"
63
- )
61
+ cipher = DES3.new(base64.b64decode(self.secret_key), DES3.MODE_CBC, IV=b"\0\0\0\0\0\0\0\0")
64
62
  # the cipher needs to be passed 16 bytes,
65
63
  # so "order" must be 16 bytes long,
66
64
  # therefore we left-justify adding ceros
@@ -96,16 +94,12 @@ class RedirectClient(Client):
96
94
  """
97
95
  decoded_parameters = self.decode_parameters(merchant_parameters.encode())
98
96
  response = Response(decoded_parameters)
99
- calculated_signature = self.generate_signature(
100
- response.order, merchant_parameters.encode()
101
- )
97
+ calculated_signature = self.generate_signature(response.order, merchant_parameters.encode())
102
98
  # Remove any non-alphanumeric characters from the signature
103
99
  not_alphanumeric = re.compile("[^a-zA-Z0-9]")
104
100
  safe_signature = re.sub(not_alphanumeric, "", signature)
105
101
 
106
- safe_calculated_signature = re.sub(
107
- not_alphanumeric, "", calculated_signature.decode()
108
- )
102
+ safe_calculated_signature = re.sub(not_alphanumeric, "", calculated_signature.decode())
109
103
  if safe_signature != safe_calculated_signature:
110
104
  raise ValueError("The provided signature is not valid.")
111
105
  return response
redsys/request.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import re
2
2
  from decimal import Decimal
3
- from typing import Any, Dict
3
+ from typing import Any
4
+ from typing import Dict
4
5
 
5
- from redsys.constants import CURRENCIES, LANGUAGES, TRANSACTIONS
6
+ from redsys.constants import CURRENCIES
7
+ from redsys.constants import LANGUAGES
8
+ from redsys.constants import TRANSACTIONS
6
9
 
7
10
  # General parameters
8
11
  MERCHANT_CODE = "Ds_Merchant_MerchantCode"
@@ -36,6 +39,10 @@ PAN = "Ds_Merchant_Pan"
36
39
  EXPIRY_DATE = "Ds_Merchant_ExpiryDate"
37
40
  CVV2 = "Ds_Merchant_Cvv2"
38
41
 
42
+ # BIZUM PAYMENT parameters
43
+ MOBILE_NUMBER = "Ds_Merchant_Bizum_MobileNumber"
44
+ PAYMENT_METHOD = "Ds_Merchant_PayMethods"
45
+
39
46
  MERCHANT_PARAMETERS_MAP = {
40
47
  "merchant_code": MERCHANT_CODE,
41
48
  "terminal": TERMINAL,
@@ -59,6 +66,8 @@ MERCHANT_PARAMETERS_MAP = {
59
66
  "pan": PAN,
60
67
  "expiry_date": EXPIRY_DATE,
61
68
  "cvv2": CVV2,
69
+ "mobile_number": MOBILE_NUMBER,
70
+ "payment_method": PAYMENT_METHOD,
62
71
  }
63
72
 
64
73
 
@@ -68,9 +77,11 @@ class Request:
68
77
  their values according to the platform specifications
69
78
  """
70
79
 
71
- _parameters: Dict[str, Any] = {}
80
+ _parameters: Dict[str, Any]
72
81
 
73
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", {})
74
85
  for key, value in parameters.items():
75
86
  if key in MERCHANT_PARAMETERS_MAP:
76
87
  if check := getattr(self, f"check_{str(key)}", None):
@@ -81,7 +92,7 @@ class Request:
81
92
 
82
93
  def __getattr__(self, item: str) -> Any:
83
94
  if item in MERCHANT_PARAMETERS_MAP:
84
- return self._parameters[item]
95
+ return self._parameters.get(item)
85
96
 
86
97
  def __setattr__(self, key, value):
87
98
  if key in MERCHANT_PARAMETERS_MAP:
@@ -93,9 +104,7 @@ class Request:
93
104
  parameters = {}
94
105
  for key, value in self._parameters.items():
95
106
  prepare = getattr(self, "prepare_%s" % key, None)
96
- parameters[MERCHANT_PARAMETERS_MAP[key]] = (
97
- prepare(value) if prepare else value
98
- )
107
+ parameters[MERCHANT_PARAMETERS_MAP[key]] = prepare(value) if prepare else value
99
108
  return parameters
100
109
 
101
110
  @staticmethod
@@ -108,7 +117,7 @@ class Request:
108
117
 
109
118
  @staticmethod
110
119
  def check_order(value):
111
- if not re.match(r"[0-9]{4}[a-zA-Z0-9]{5}$", value):
120
+ if not re.match(r"[0-9]{4}[a-zA-Z0-9]{8}$", value):
112
121
  raise ValueError("order format is not valid.")
113
122
 
114
123
  @staticmethod
redsys/response.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from decimal import Decimal
2
- from typing import Any, Dict
2
+ from typing import Any
3
+ from typing import Dict
3
4
 
4
5
  RESPONSE = "Ds_Response"
5
6
  DATE = "Ds_Date"
@@ -92,12 +93,12 @@ class Response:
92
93
  Defines a response
93
94
  """
94
95
 
95
- _parameters: Dict[str, Any] = {}
96
+ _parameters: Dict[str, Any]
96
97
 
97
98
  def __init__(self, parameters: Dict[str, Any]):
98
- MERCHANT_PARAMETERS_MAP_REVERSE = {
99
- value: key for key, value in MERCHANT_PARAMETERS_MAP.items()
100
- }
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()}
101
102
  for key, value in parameters.items():
102
103
  reversed_parameter = MERCHANT_PARAMETERS_MAP_REVERSE.get(key, key)
103
104
  clean = getattr(self, "clean_%s" % reversed_parameter, None)
@@ -105,14 +106,12 @@ class Response:
105
106
 
106
107
  def __getattr__(self, item: str) -> Any:
107
108
  if item in MERCHANT_PARAMETERS_MAP:
108
- return self._parameters[item]
109
+ return self._parameters.get(item)
109
110
 
110
111
  def __setattr__(self, key: str, value: Any):
111
112
  if key in MERCHANT_PARAMETERS_MAP:
112
113
  clean = getattr(self, "clean_%s" % MERCHANT_PARAMETERS_MAP[key], None)
113
- self._parameters[MERCHANT_PARAMETERS_MAP[key]] = (
114
- clean(value) if clean else value
115
- )
114
+ self._parameters[MERCHANT_PARAMETERS_MAP[key]] = clean(value) if clean else value
116
115
 
117
116
  @property
118
117
  def is_authorized(self):
@@ -144,4 +143,4 @@ class Response:
144
143
 
145
144
  @staticmethod
146
145
  def clean_amount(value):
147
- return Decimal("{}.{}".format(str(value)[:-2], str(value)[-2:]))
146
+ return Decimal(f"{str(value)[:-2]}.{str(value)[-2:]}")
@@ -4,7 +4,8 @@ from decimal import Decimal as D
4
4
  import pytest
5
5
 
6
6
  from redsys.client import RedirectClient
7
- from redsys.constants import EUR, STANDARD_PAYMENT
7
+ from redsys.constants import EUR
8
+ from redsys.constants import STANDARD_PAYMENT
8
9
  from redsys.request import Request
9
10
 
10
11
 
@@ -18,7 +19,7 @@ class TestRedirectClient:
18
19
  "terminal": "1",
19
20
  "transaction_type": STANDARD_PAYMENT,
20
21
  "currency": EUR,
21
- "order": "000000001",
22
+ "order": "000000000001",
22
23
  "amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
23
24
  "merchant_data": "test merchant data",
24
25
  "merchant_name": "Example Commerce",
@@ -33,7 +34,7 @@ class TestRedirectClient:
33
34
  encoded_params = self.client.encode_parameters(self.merchant_params)
34
35
  encoded_params_str = encoded_params.decode("UTF-8")
35
36
  assert encoded_params_str.endswith("ZWRzeXMvcmVzcG9uc2UifQ==")
36
- assert len(encoded_params_str) == 624
37
+ assert len(encoded_params_str) == 628
37
38
 
38
39
  def test_decode_parameters(self):
39
40
  encoded_params = self.client.encode_parameters(self.merchant_params)
@@ -43,24 +44,18 @@ class TestRedirectClient:
43
44
  def test_encrypt_3DES(self):
44
45
  encrypted_order = self.client.encrypt_3DES(self.request.order)
45
46
  assert len(encrypted_order) == 16
46
- assert (
47
- str(encrypted_order)
48
- == 'b"/\\x17\\\\6\\n\\x1aY\\xf3\\xb7\\\\\\xde\'d\\xa5\\x03\\xb7"'
49
- )
47
+ assert str(encrypted_order) == "b'/\\x17\\\\6\\n\\x1aY\\xf3\\xd7\\xa5\\x97\\xb9\\xb6\\xe0\\x8dH'"
50
48
 
51
49
  def test_sign_hmac256(self):
52
50
  encrypted_order = self.client.encrypt_3DES(self.request.order)
53
51
  encoded_params = self.client.encode_parameters(self.merchant_params)
54
52
  signature = self.client.sign_hmac256(encrypted_order, encoded_params)
55
- assert signature == b"eEW4YN7/kkNHRO0/HhXy9TppzHwF38+eZApKAagDI9A="
53
+ assert signature == b"a2F05DwuH5aXu5vD1IdjQ+NxaR0lPsRwksW1nr2Nzuw="
56
54
 
57
55
  def test_prepare_request(self):
58
56
  prepared_request = self.client.prepare_request(self.parameters)
59
57
  assert len(prepared_request.keys()) == 3
60
- assert (
61
- prepared_request.get("Ds_Signature")
62
- == b"eEW4YN7/kkNHRO0/HhXy9TppzHwF38+eZApKAagDI9A="
63
- )
58
+ assert prepared_request.get("Ds_Signature") == b"a2F05DwuH5aXu5vD1IdjQ+NxaR0lPsRwksW1nr2Nzuw="
64
59
  pass
65
60
 
66
61
  def test_create_response_valid(self):
@@ -69,15 +64,13 @@ class TestRedirectClient:
69
64
  "Ds_Terminal": "1",
70
65
  "Ds_TransactionType": "0",
71
66
  "Ds_Currency": 978,
72
- "Ds_Order": "000000001",
67
+ "Ds_Order": "000000000001",
73
68
  "Ds_Amount": 1056,
74
69
  "Ds_MerchantData": "test merchant data",
75
70
  }
76
71
  encoded_params = self.client.encode_parameters(merchant_parameters)
77
72
  signature = self.client.generate_signature(self.request.order, encoded_params)
78
- response = self.client.create_response(
79
- signature.decode(), encoded_params.decode()
80
- )
73
+ response = self.client.create_response(signature.decode(), encoded_params.decode())
81
74
  assert response._parameters.get("order") == merchant_parameters.get("Ds_Order")
82
75
 
83
76
  def test_create_response_invalid(self):
@@ -86,7 +79,7 @@ class TestRedirectClient:
86
79
  "Ds_Terminal": "1",
87
80
  "Ds_TransactionType": "0",
88
81
  "Ds_Currency": 978,
89
- "Ds_Order": "000000001",
82
+ "Ds_Order": "000000000001",
90
83
  "Ds_Amount": 1056,
91
84
  "Ds_MerchantData": "test merchant data",
92
85
  }
@@ -4,7 +4,8 @@ from random import choice
4
4
 
5
5
  import pytest
6
6
 
7
- from redsys.constants import EUR, STANDARD_PAYMENT
7
+ from redsys.constants import EUR
8
+ from redsys.constants import STANDARD_PAYMENT
8
9
  from redsys.request import Request
9
10
 
10
11
 
@@ -15,7 +16,7 @@ class TestRequest:
15
16
  "terminal": "1",
16
17
  "transaction_type": STANDARD_PAYMENT,
17
18
  "currency": EUR,
18
- "order": "000000001",
19
+ "order": "000000000001",
19
20
  "amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
20
21
  "merchant_data": "test merchant data",
21
22
  "merchant_name": "Example Commerce",
@@ -41,7 +42,7 @@ class TestRequest:
41
42
  "terminal": "1",
42
43
  "transaction_type": STANDARD_PAYMENT,
43
44
  "currency": EUR,
44
- "order": "000000001",
45
+ "order": "000000000001",
45
46
  "amount": D("10.56489").quantize(D(".01"), ROUND_HALF_UP),
46
47
  "merchant_data": "test merchant data",
47
48
  "merchant_name": "Example Commerce",
@@ -51,20 +52,23 @@ class TestRequest:
51
52
  }
52
53
  prepared_parameters = Request(parameters).prepare_parameters()
53
54
  assert len(prepared_parameters.keys()) == 11
54
- assert (
55
- prepared_parameters.get("Ds_Merchant_MerchantData") == "test merchant data"
56
- )
55
+ assert prepared_parameters.get("Ds_Merchant_MerchantData") == "test merchant data"
57
56
 
58
57
  def test_super_long_merchant_data_throws_error(self):
59
- merchant_data = "".join(
60
- choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(1025)
61
- )
58
+ merchant_data = "".join(choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(1025))
62
59
  with pytest.raises(ValueError):
63
60
  assert Request.check_merchant_data(merchant_data)
64
61
 
65
62
  def test_super_long_merchant_url_throws_error(self):
66
- merchant_url = "".join(
67
- choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(251)
68
- )
63
+ merchant_url = "".join(choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(251))
69
64
  with pytest.raises(ValueError):
70
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
 
@@ -8,18 +10,23 @@ class TestResponse:
8
10
  "Ds_MerchantCode": "1056",
9
11
  "Ds_Terminal": "1",
10
12
  "Ds_TransactionType": "1",
11
- "Ds_Order": "000000001",
13
+ "Ds_Order": "000000000001",
12
14
  "Ds_Amount": "10054",
13
15
  "Ds_MerchantData": "test merchant data",
14
16
  "Ds_Currency": 978,
15
17
  }
16
18
  response = Response(parameters)
17
19
  assert response.code == 90
18
- assert (
19
- response.message == "Transacción autorizada para pagos y preautorizaciones"
20
- )
20
+ assert response.message == "Transacción autorizada para pagos y preautorizaciones"
21
21
  assert len(response._parameters.keys()) == 8
22
22
  assert response.is_authorized is True
23
23
  assert response.is_paid is True
24
24
  assert response.is_canceled is False
25
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")
@@ -1,13 +0,0 @@
1
- redsys/__init__.py,sha256=isoYrgHz945c1Cgxa2CY6lzHZXM3NV2r_KSvgTWKD6k,148
2
- redsys/client.py,sha256=-wFN9XpKqa-Y3R4InWj-mq_Jtt4-IhO0nbaf4TiQsZM,3992
3
- redsys/constants.py,sha256=lJYgbmikxh0b9iVM4M5XFWBWVvdFY27OkcXSLluHSsQ,2497
4
- redsys/request.py,sha256=NuOH-ReP2xw23rtBS5vE1L9lk0BCr7qR4A6SPGJcEqc,4919
5
- redsys/response.py,sha256=Yz7BvnsHY9UBencH1dO27di5MkVtrZ9SbTBE4QNNP78,5326
6
- redsys/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- redsys/tests/test_client.py,sha256=7Ra-xYx0dZcSbe8ReszzEM1jD7CW6XDMTqOULtnAnvg,3911
8
- redsys/tests/test_request.py,sha256=p0zstJwi4_-PL70tQXgNFskp7P5VBCxlDf9ZAaWAOe8,2635
9
- redsys/tests/test_response.py,sha256=pi-iyAZ2Vc7MF4IUMM8Idztk7d5qD0C7GTkVbX4jrIo,847
10
- python_redsys-1.1.1.dist-info/LICENSE,sha256=Ku6mJcUOcHd2aZ4o2Jh4Tjkx1oZT-iT0VCyUNvm2IXU,1257
11
- python_redsys-1.1.1.dist-info/WHEEL,sha256=y3eDiaFVSNTPbgzfNn0nYn5tEn1cX6WrdetDlQM4xWw,83
12
- python_redsys-1.1.1.dist-info/METADATA,sha256=ym0S75ZhTxDrbFdrKDjg74AziZ5Sd_dJevIeaWThvXs,4412
13
- python_redsys-1.1.1.dist-info/RECORD,,