python-redsys 2.0.0__py3-none-any.whl → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-redsys
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A simple, lightweight client to handle payments through the RedSys platform.
5
5
  Author-email: David Díaz <d.diazp@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -48,6 +48,17 @@ pip install "python-redsys[http]"
48
48
 
49
49
  Requires Python 3.10+. Runtime dependencies: `cryptography` and `pydantic` (v2).
50
50
 
51
+ ## Status
52
+
53
+ | | |
54
+ |---|---|
55
+ | Version | 2.1.0; 1.x receives fixes until 2026-12-31 |
56
+ | Python | 3.10 to 3.15 |
57
+ | RedSys documents followed | Redirection guide 4.2 (2026-02-04), REST guide 4.0.1 (2025-10-17), Bizum guide 2.6 (2026-08-25) |
58
+ | Signature versions | `HMAC_SHA512_V2` (default), `HMAC_SHA512_V1`, `HMAC_SHA256_V1` |
59
+ | Verified against the RedSys test environment | Redirect connection in a browser, with the 3DS challenge, for all three signature versions (2026-10-25); direct connection end to end (3DS initialization, challenged payment, refund, stored-card token charged as a merchant-initiated payment) on demand by the *Live* workflow |
60
+ | Not yet verified live | Frictionless 3DS and the 3DSMethod step |
61
+
51
62
  ## Upgrading from 1.x
52
63
 
53
64
  2.0 has a new API. The [migration guide](docs/migration-1x-to-2x.md) lists every change; 1.x receives fixes until 2026-12-31.
@@ -194,7 +205,7 @@ client.payment(
194
205
  ).send()
195
206
  ```
196
207
 
197
- `redsys.cof` provides the indicator constants (`INITIAL`/`SUBSEQUENT`, `RECURRING`/`INSTALLMENT`/...) and the `REQUEST_TOKEN` sentinel. The periodic fields (`sum_total`, `date_frequency`, `charge_expiry_date`) are also accepted by `DirectClient.payment()`. `redsys.sca` holds the PSD2 exemption codes for `excep_sca` (`LWV`, `TRA`, `MIT`, `COR`, `ATD`); `sca.QUERY` on `inicia()` asks which exemptions the card allows, returned in `response.excep_sca`.
208
+ `redsys.cof` provides the indicator constants (`INITIAL`/`SUBSEQUENT`, `RECURRING`/`INSTALLMENT`/...) and the `REQUEST_TOKEN` sentinel. The token and COF fields are also accepted by `RedirectClient.payment()`, so the first payment can run on the RedSys page. The periodic fields (`sum_total`, `date_frequency`, `charge_expiry_date`) are accepted by `DirectClient.payment()`. `redsys.sca` holds the PSD2 exemption codes for `excep_sca` (`LWV`, `TRA`, `MIT`, `COR`, `ATD`); `sca.QUERY` on `inicia()` asks which exemptions the card allows, returned in `response.excep_sca`.
198
209
 
199
210
  ## Signature versions
200
211
 
@@ -1,10 +1,10 @@
1
- python_redsys-2.0.0.dist-info/licenses/LICENSE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
2
- python_redsys-2.0.0.dist-info/licenses/NOTICE,sha256=6tlv5QuzsbfmGxsQNEcN7Uopzyp7vgD54xb-_L4DGyE,201
1
+ python_redsys-2.1.0.dist-info/licenses/LICENSE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
2
+ python_redsys-2.1.0.dist-info/licenses/NOTICE,sha256=6tlv5QuzsbfmGxsQNEcN7Uopzyp7vgD54xb-_L4DGyE,201
3
3
  redsys/__init__.py,sha256=bhpyzXB0Y-3FORBJlV4dZ3vnrz1jSc-sbZ9hyb7itFo,138
4
- redsys/clients.py,sha256=4_lM6c6ofb13z_GNMa4d7pTCn2R6zbSRYrKJLfNdejA,10710
4
+ redsys/clients.py,sha256=b9K4jCSLC9JNWUS8wVFqkc9im_a5ukSA-JpunbhKEBc,11067
5
5
  redsys/crypto.py,sha256=GOm8RkDYnD83fMNwey-bxLg3vWf6xbNzWbza6m23M9M,5480
6
6
  redsys/exceptions.py,sha256=MqknsHm4sI5iAelrYcAuqSC70u1hhqdLyvA27vKcLHY,601
7
- redsys/models.py,sha256=UzvCBrYgwixTS-b4PjMaeLCg7txNlWkcs0vmWeX94SM,15765
7
+ redsys/models.py,sha256=9YqPBUc0LgEEFhvZiwzdM16w2DyqICF5DnXrvrVK4Po,16653
8
8
  redsys/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
9
  redsys/request.py,sha256=dVAQ0zSVmdyUPjnERo_62xilNQk3-LGJTbR6im_y0fQ,2408
10
10
  redsys/threeds.py,sha256=IheD2mWVD9Pw7efRP6_a170LVppSSlKpyxhUJVI3LnU,2244
@@ -13,8 +13,8 @@ redsys/api/cof.py,sha256=fEf9ugVvOMs-jAzXkXDQl4NshyZuSYvbYfqnRH8otrk,660
13
13
  redsys/api/currencies.py,sha256=GufqxjAb941KMZ2S2mJJ980Xz4naJ2GgkcOVwuPx0ms,1043
14
14
  redsys/api/languages.py,sha256=GPw7JB-CWDyGo6vhcPZDzpjGuwkojFCz1gyfx13wlek,872
15
15
  redsys/api/sca.py,sha256=09b9LV9dvBADDTfHnReMR3OuM38ViVRqQs_hLQFBEoE,786
16
- redsys/api/transactions.py,sha256=HYmaEXsfAb9-Kc9WlTGTetEA23vM1aGsQQua2Ukk3PM,732
17
- python_redsys-2.0.0.dist-info/METADATA,sha256=lWRxu0v6tYC3RHfS3Rp29hXH8zL6c6scMm7anIUVY3k,9793
18
- python_redsys-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
19
- python_redsys-2.0.0.dist-info/top_level.txt,sha256=79TrHj--FP5SDyZO42-NUsiYJmHiQX42txZigHOqghY,7
20
- python_redsys-2.0.0.dist-info/RECORD,,
16
+ redsys/api/transactions.py,sha256=MzBdsgn_27_nayxCavs15tXQShjm4NsGbQVAeTn9oTg,1633
17
+ python_redsys-2.1.0.dist-info/METADATA,sha256=Q9DeegOyNcRZ76kari8o2O9Rh_d-DOHt2Qfu7yk9tJk,10632
18
+ python_redsys-2.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
19
+ python_redsys-2.1.0.dist-info/top_level.txt,sha256=79TrHj--FP5SDyZO42-NUsiYJmHiQX42txZigHOqghY,7
20
+ python_redsys-2.1.0.dist-info/RECORD,,
@@ -19,6 +19,9 @@ AUTHENTICATION_CONFIRMATION = 8
19
19
  CANCELLATION = 9
20
20
  """Cancellation"""
21
21
 
22
+ REPLACEMENT_PREAUTHORIZATION = 11
23
+ """Replacement preauthorization: extends an existing preauthorization."""
24
+
22
25
  PAY_GOLD = 15
23
26
  """Pay gold"""
24
27
 
@@ -31,6 +34,24 @@ REFUND_WITHOUT_ORIGINAL = 34
31
34
  BETTING_PRICE = 37
32
35
  """Betting price"""
33
36
 
37
+ PAY_GOLD_AUTHORIZATION = 38
38
+ """PayGold authorization: the cardholder completes a PayGold request (type 15); sent by RedSys, not the merchant."""
39
+
40
+ DELETE_REFERENCE = 44
41
+ """Delete a stored card reference (token)."""
42
+
43
+ PAYMENT_CANCELLATION = 45
44
+ """Payment cancellation, for merchant time-outs or technical errors."""
45
+
46
+ REFUND_CANCELLATION = 46
47
+ """Refund cancellation, within 48 hours of the refund."""
48
+
49
+ AUTHENTICATION_CONFIRMATION_CANCELLATION = 47
50
+ """Cancellation of an authentication confirmation (type 8)."""
51
+
52
+ PARTIAL_CONFIRMATION = 48
53
+ """Partial confirmation of a preauthorization."""
54
+
34
55
  TRANSACTION_TYPES = (
35
56
  AUTHORIZATION,
36
57
  PREAUTHORIZATION,
@@ -39,8 +60,15 @@ TRANSACTION_TYPES = (
39
60
  AUTHENTICATION,
40
61
  AUTHENTICATION_CONFIRMATION,
41
62
  CANCELLATION,
63
+ REPLACEMENT_PREAUTHORIZATION,
42
64
  PAY_GOLD,
43
65
  AUTHENTICATION_PUCE,
44
66
  REFUND_WITHOUT_ORIGINAL,
45
67
  BETTING_PRICE,
68
+ PAY_GOLD_AUTHORIZATION,
69
+ DELETE_REFERENCE,
70
+ PAYMENT_CANCELLATION,
71
+ REFUND_CANCELLATION,
72
+ AUTHENTICATION_CONFIRMATION_CANCELLATION,
73
+ PARTIAL_CONFIRMATION,
46
74
  )
redsys/clients.py CHANGED
@@ -209,6 +209,11 @@ class _DirectBase(Client):
209
209
  transactions.CONFIRMATION: ConfirmationMessage,
210
210
  transactions.REFUND: RefundMessage,
211
211
  transactions.CANCELLATION: CancellationMessage,
212
+ transactions.REPLACEMENT_PREAUTHORIZATION: DirectPreauthorizationMessage,
213
+ transactions.PARTIAL_CONFIRMATION: ConfirmationMessage,
214
+ transactions.PAYMENT_CANCELLATION: CancellationMessage,
215
+ transactions.REFUND_CANCELLATION: CancellationMessage,
216
+ transactions.AUTHENTICATION_CONFIRMATION_CANCELLATION: CancellationMessage,
212
217
  }
213
218
 
214
219
  @property
redsys/models.py CHANGED
@@ -20,7 +20,7 @@ from json import dumps, loads
20
20
  from typing import Any
21
21
  from warnings import warn
22
22
 
23
- from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
23
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
24
24
 
25
25
  from redsys.api.currencies import CURRENCIES, CURRENCY_DECIMALS
26
26
  from redsys.api.transactions import TRANSACTION_TYPES
@@ -43,10 +43,14 @@ def decode_merchant_parameters(data: bytes) -> dict:
43
43
  return loads(b64decode(data + b'=' * (-len(data) % 4)).decode('utf-8'))
44
44
 
45
45
 
46
+ def currency_decimals(currency: int | None) -> int:
47
+ """ISO-4217 minor-unit exponent of a currency; 2 when unknown."""
48
+ return CURRENCY_DECIMALS.get(currency, 2) if currency is not None else 2
49
+
50
+
46
51
  def minor_units_factor(currency: int | None) -> int:
47
52
  """Power-of-ten factor that converts major units to the wire minor units."""
48
- decimals = CURRENCY_DECIMALS.get(currency, 2) if currency is not None else 2
49
- return 10**decimals
53
+ return 10 ** currency_decimals(currency)
50
54
 
51
55
 
52
56
  class BaseRequest(BaseModel):
@@ -117,7 +121,19 @@ class BaseRequest(BaseModel):
117
121
  return b64encode(dumps(self.prepare_parameters(), sort_keys=True).encode('utf-8'))
118
122
 
119
123
 
120
- class _RedirectFields(BaseModel):
124
+ class _StoredCredentialFields(BaseModel):
125
+ """Tokenization and credential-on-file fields, accepted by both connections."""
126
+
127
+ identifier: str | None = Field(
128
+ None, alias='Ds_Merchant_Identifier', max_length=40, description='Stored-card token; see redsys.api.cof.'
129
+ )
130
+ direct_payment: str | None = Field(None, alias='Ds_Merchant_DirectPayment', description='"true" or "moto".')
131
+ cof_ini: str | None = Field(None, alias='Ds_Merchant_Cof_Ini', max_length=1, description='See redsys.api.cof.')
132
+ cof_type: str | None = Field(None, alias='Ds_Merchant_Cof_Type', max_length=1, description='See redsys.api.cof.')
133
+ cof_txnid: str | None = Field(None, alias='Ds_Merchant_Cof_Txnid', max_length=15)
134
+
135
+
136
+ class _RedirectFields(_StoredCredentialFields):
121
137
  """Fields of the redirect connection (browser form)."""
122
138
 
123
139
  merchant_url: str | None = Field(None, alias='Ds_Merchant_MerchantURL', max_length=250)
@@ -137,24 +153,20 @@ class _RedirectFields(BaseModel):
137
153
  )
138
154
 
139
155
 
140
- class _DirectFields(BaseModel):
156
+ class _DirectFields(_StoredCredentialFields):
141
157
  """Fields of the direct connection (server-to-server card data, 3DS, stored credentials)."""
142
158
 
143
159
  pan: str | None = Field(None, alias='Ds_Merchant_Pan', max_length=19, pattern=r'^[0-9]{12,19}$')
144
160
  expiry_date: str | None = Field(None, alias='Ds_Merchant_ExpiryDate', pattern=r'^[0-9]{4}$', description='YYMM.')
145
161
  cvv2: str | None = Field(None, alias='Ds_Merchant_Cvv2', pattern=r'^[0-9]{3,4}$')
146
- identifier: str | None = Field(
147
- None, alias='Ds_Merchant_Identifier', max_length=40, description='Stored-card token; see redsys.api.cof.'
148
- )
149
162
  idoper: str | None = Field(None, alias='Ds_Merchant_Idoper', max_length=40, description='inSite operation token.')
150
- direct_payment: str | None = Field(None, alias='Ds_Merchant_DirectPayment', description='"true" or "moto".')
151
163
  emv3ds: dict[str, Any] | None = Field(None, alias='Ds_Merchant_EMV3DS', description='Build with redsys.threeds.')
152
- cof_ini: str | None = Field(None, alias='Ds_Merchant_Cof_Ini', max_length=1, description='See redsys.api.cof.')
153
- cof_type: str | None = Field(None, alias='Ds_Merchant_Cof_Type', max_length=1, description='See redsys.api.cof.')
154
- cof_txnid: str | None = Field(None, alias='Ds_Merchant_Cof_Txnid', max_length=15)
155
164
  # RedSys spells the wire name "Frecuency".
156
165
  date_frequency: str | None = Field(None, alias='Ds_Merchant_DateFrecuency', description='Days between charges.')
157
166
  charge_expiry_date: str | None = Field(None, alias='Ds_Merchant_ChargeExpiryDate', description='yyyy-MM-dd.')
167
+ dcc: str | dict[str, Any] | None = Field(
168
+ None, alias='Ds_Merchant_DCC', description='"Y" on the 3DS initialization; then the accepted offer.'
169
+ )
158
170
  excep_sca: str | None = Field(
159
171
  None,
160
172
  alias='Ds_Merchant_Excep_SCA',
@@ -243,13 +255,17 @@ class Response(BaseModel):
243
255
  secure_payment: str | None = Field(None, alias='Ds_SecurePayment')
244
256
  authorization_code: str | None = Field(None, alias='Ds_AuthorisationCode')
245
257
  error_code: str | None = Field(None, alias='Ds_ErrorCode')
246
- card_number: str | None = Field(None, alias='Ds_CardNumber')
258
+ # REST responses send Ds_CardNumber; redirect notifications send Ds_Card_Number.
259
+ card_number: str | None = Field(
260
+ None, alias='Ds_CardNumber', validation_alias=AliasChoices('Ds_CardNumber', 'Ds_Card_Number', 'card_number')
261
+ )
247
262
  expiry_date: str | None = Field(None, alias='Ds_ExpiryDate')
248
263
  card_psd2: str | None = Field(None, alias='Ds_Card_PSD2')
249
264
  excep_sca: str | None = Field(
250
265
  None, alias='Ds_Excep_SCA', description='Exemptions the card allows, from the 3DS initialization.'
251
266
  )
252
267
  emv3ds: dict[str, Any] | None = Field(None, alias='Ds_EMV3DS')
268
+ dcc: dict[str, Any] | None = Field(None, alias='Ds_DCC', description='Offer from the 3DS initialization.')
253
269
  identifier: str | None = Field(None, alias='Ds_Merchant_Identifier', description='Token returned on request.')
254
270
  cof_txnid: str | None = Field(None, alias='Ds_Merchant_Cof_Txnid')
255
271
  processed_pay_method: str | None = Field(None, alias='Ds_ProcessedPayMethod', description='68 = Bizum.')
@@ -269,7 +285,8 @@ class Response(BaseModel):
269
285
  def from_merchant_parameters(cls, merchant_parameters: bytes) -> 'Response':
270
286
  response = cls.model_validate(decode_merchant_parameters(merchant_parameters))
271
287
  if response.amount is not None:
272
- response.amount = response.amount / minor_units_factor(response.currency)
288
+ # scaleb keeps the currency's decimals: 100 EUR minor units read as 1.00, not 1.
289
+ response.amount = response.amount.scaleb(-currency_decimals(response.currency))
273
290
  return response
274
291
 
275
292
  def get_order(self) -> str | None:
@@ -336,5 +353,6 @@ __all__ = [
336
353
  'CancellationMessage',
337
354
  'Response',
338
355
  'decode_merchant_parameters',
356
+ 'currency_decimals',
339
357
  'minor_units_factor',
340
358
  ]