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.
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/METADATA +13 -2
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/RECORD +9 -9
- redsys/api/transactions.py +28 -0
- redsys/clients.py +5 -0
- redsys/models.py +32 -14
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/WHEEL +0 -0
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/licenses/NOTICE +0 -0
- {python_redsys-2.0.0.dist-info → python_redsys-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-redsys
|
|
3
|
-
Version: 2.
|
|
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
|
|
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.
|
|
2
|
-
python_redsys-2.
|
|
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
|
+
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=
|
|
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=
|
|
17
|
-
python_redsys-2.
|
|
18
|
-
python_redsys-2.
|
|
19
|
-
python_redsys-2.
|
|
20
|
-
python_redsys-2.
|
|
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,,
|
redsys/api/transactions.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|