python-redsys 2.1.0__py3-none-any.whl → 2.2.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.2.0.dist-info/METADATA +187 -0
- python_redsys-2.2.0.dist-info/RECORD +34 -0
- redsys/__init__.py +38 -2
- redsys/_code_texts.py +4673 -0
- redsys/api/__init__.py +1 -1
- redsys/api/bizum.py +71 -0
- redsys/api/cards.py +50 -0
- redsys/api/currencies.py +18 -18
- redsys/api/emv3ds.py +99 -0
- redsys/api/environments.py +41 -0
- redsys/api/languages.py +9 -1
- redsys/api/options.py +31 -0
- redsys/api/pay_methods.py +25 -0
- redsys/api/paygold.py +33 -0
- redsys/api/processed_pay_methods.py +207 -0
- redsys/api/responses.py +290 -0
- redsys/api/sca.py +11 -3
- redsys/api/secure_payment.py +12 -0
- redsys/api/transactions.py +26 -9
- redsys/api/xpay.py +25 -0
- redsys/clients.py +265 -85
- redsys/codes.py +76 -0
- redsys/crypto.py +8 -8
- redsys/exceptions.py +5 -2
- redsys/models.py +283 -49
- redsys/orders.py +20 -0
- redsys/request.py +19 -13
- redsys/threeds.py +21 -16
- python_redsys-2.1.0.dist-info/METADATA +0 -225
- python_redsys-2.1.0.dist-info/RECORD +0 -20
- {python_redsys-2.1.0.dist-info → python_redsys-2.2.0.dist-info}/WHEEL +0 -0
- {python_redsys-2.1.0.dist-info → python_redsys-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_redsys-2.1.0.dist-info → python_redsys-2.2.0.dist-info}/licenses/NOTICE +0 -0
- {python_redsys-2.1.0.dist-info → python_redsys-2.2.0.dist-info}/top_level.txt +0 -0
redsys/models.py
CHANGED
|
@@ -9,9 +9,13 @@ dict with ``model_validate``. Field constraints follow these RedSys documents:
|
|
|
9
9
|
- TPV Virtual, Manual de Integración REST, RS.TE.CEL.MAN.0037, version 4.0.1 (2025-10-17),
|
|
10
10
|
https://pagosonline.redsys.es/download/1738/
|
|
11
11
|
- Guía de integración de Bizum, version 2.6 (2026-08-25), https://pagosonline.redsys.es/download/2053/
|
|
12
|
+
- inSite guide, version 4.1 (2026-01-05), https://pagosonline.redsys.es/download/1751/
|
|
13
|
+
- Pago por e-mail/SMS (PayGold), RS.TE.CEL.MAN.0026, version 1.8.2 (2026-04-20),
|
|
14
|
+
https://pagosonline.redsys.es/download/2056/
|
|
15
|
+
- Integración de Google Pay, version 1.9 (2025-05-21), https://pagosonline.redsys.es/download/2045/
|
|
16
|
+
- Apple Pay, guía de instalación, version 3.2 (2023-03-28), https://pagosonline.redsys.es/download/2050/
|
|
12
17
|
- Developer portal pages "Firmar una operación", "Parámetros de entrada y salida" and "Operativa DCC".
|
|
13
|
-
-
|
|
14
|
-
for the response fields the guides omit.
|
|
18
|
+
- Responses of the RedSys test environment, for the response fields the guides omit.
|
|
15
19
|
"""
|
|
16
20
|
|
|
17
21
|
from base64 import b64decode, b64encode
|
|
@@ -20,17 +24,27 @@ from json import dumps, loads
|
|
|
20
24
|
from typing import Any
|
|
21
25
|
from warnings import warn
|
|
22
26
|
|
|
23
|
-
from pydantic import
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
from pydantic import (
|
|
28
|
+
AliasChoices,
|
|
29
|
+
BaseModel,
|
|
30
|
+
ConfigDict,
|
|
31
|
+
Field,
|
|
32
|
+
PrivateAttr,
|
|
33
|
+
field_serializer,
|
|
34
|
+
field_validator,
|
|
35
|
+
model_validator,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from redsys.api import emv3ds, options, paygold, responses, transactions, xpay
|
|
39
|
+
from redsys.api.currencies import CURRENCIES, CURRENCY_DECIMALS, EUR
|
|
26
40
|
from redsys.api.transactions import TRANSACTION_TYPES
|
|
41
|
+
from redsys.codes import describe
|
|
27
42
|
from redsys.exceptions import UnknownParameterWarning
|
|
28
43
|
|
|
29
|
-
# 1.x request names that changed
|
|
30
|
-
_LEGACY_FIELDS: dict[str, str
|
|
44
|
+
# 1.x request names that changed in 2.0; the error names the replacement.
|
|
45
|
+
_LEGACY_FIELDS: dict[str, str] = {
|
|
31
46
|
'payment_method': 'pay_methods',
|
|
32
47
|
'mobile_number': 'bizum_mobile_number',
|
|
33
|
-
'transaction_date': None,
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
|
|
@@ -56,26 +70,22 @@ def minor_units_factor(currency: int | None) -> int:
|
|
|
56
70
|
class BaseRequest(BaseModel):
|
|
57
71
|
"""Fields common to every operation message, with validation and wire serialization."""
|
|
58
72
|
|
|
59
|
-
#
|
|
73
|
+
# extra='allow' keeps the unmodeled wire names that _screen_unknown_fields lets through.
|
|
60
74
|
model_config = ConfigDict(populate_by_name=True, extra='allow')
|
|
61
75
|
|
|
62
76
|
merchant_code: str = Field(alias='Ds_Merchant_MerchantCode', max_length=9)
|
|
63
77
|
terminal: int = Field(alias='Ds_Merchant_Terminal', ge=1, le=999)
|
|
64
78
|
transaction_type: int = Field(0, alias='Ds_Merchant_TransactionType')
|
|
65
79
|
amount: Decimal = Field(alias='Ds_Merchant_Amount', description='Major units; scaled to minor units on the wire.')
|
|
66
|
-
currency: int = Field(alias='Ds_Merchant_Currency')
|
|
80
|
+
currency: int = Field(EUR, alias='Ds_Merchant_Currency')
|
|
67
81
|
order: str = Field(alias='Ds_Merchant_Order', min_length=4, max_length=12, pattern=r'^[0-9]{4}[a-zA-Z0-9]{0,8}$')
|
|
68
82
|
merchant_data: str | None = Field(None, alias='Ds_Merchant_MerchantData', max_length=1024)
|
|
83
|
+
module: str | None = Field(None, alias='Ds_Merchant_Module', description='Integration that sends the request.')
|
|
69
84
|
|
|
70
85
|
@model_validator(mode='before')
|
|
71
86
|
@classmethod
|
|
72
87
|
def _screen_unknown_fields(cls, data: Any) -> Any:
|
|
73
|
-
"""Reject
|
|
74
|
-
|
|
75
|
-
A parameter the library does not model yet is accepted under its wire
|
|
76
|
-
name (``Ds_Merchant_*``) and sent as given, so a RedSys addition does not
|
|
77
|
-
wait for a library release. Any other unknown name is a mistake and fails.
|
|
78
|
-
"""
|
|
88
|
+
"""Reject unknown and 1.x names; let unmodeled wire names (``Ds_Merchant_*``) through with a warning."""
|
|
79
89
|
if not isinstance(data, dict):
|
|
80
90
|
return data
|
|
81
91
|
known = {name for name in cls.model_fields} | {f.alias for f in cls.model_fields.values() if f.alias}
|
|
@@ -83,9 +93,7 @@ class BaseRequest(BaseModel):
|
|
|
83
93
|
if key in known:
|
|
84
94
|
continue
|
|
85
95
|
if key in _LEGACY_FIELDS:
|
|
86
|
-
|
|
87
|
-
hint = f'use {replacement!r}' if replacement else 'it has no 2.x equivalent'
|
|
88
|
-
raise ValueError(f'{key!r} is a 1.x field name; {hint}.')
|
|
96
|
+
raise ValueError(f'{key!r} is a 1.x field name; use {_LEGACY_FIELDS[key]!r}.')
|
|
89
97
|
if key.lower().startswith('ds_merchant_'):
|
|
90
98
|
warn(f'{key!r} is not modeled by this library; sent as given.', UnknownParameterWarning, stacklevel=2)
|
|
91
99
|
continue
|
|
@@ -127,13 +135,40 @@ class _StoredCredentialFields(BaseModel):
|
|
|
127
135
|
identifier: str | None = Field(
|
|
128
136
|
None, alias='Ds_Merchant_Identifier', max_length=40, description='Stored-card token; see redsys.api.cof.'
|
|
129
137
|
)
|
|
130
|
-
direct_payment: str | None = Field(
|
|
138
|
+
direct_payment: str | None = Field(
|
|
139
|
+
None, alias='Ds_Merchant_DirectPayment', description='options.DIRECT_PAYMENT or options.MOTO.'
|
|
140
|
+
)
|
|
131
141
|
cof_ini: str | None = Field(None, alias='Ds_Merchant_Cof_Ini', max_length=1, description='See redsys.api.cof.')
|
|
132
142
|
cof_type: str | None = Field(None, alias='Ds_Merchant_Cof_Type', max_length=1, description='See redsys.api.cof.')
|
|
133
143
|
cof_txnid: str | None = Field(None, alias='Ds_Merchant_Cof_Txnid', max_length=15)
|
|
144
|
+
group: str | None = Field(
|
|
145
|
+
None, alias='Ds_Merchant_Group', pattern=r'^[0-9]{1,9}$', description='Group that shares card references.'
|
|
146
|
+
)
|
|
147
|
+
email_titular: str | None = Field(
|
|
148
|
+
None, alias='Ds_Merchant_Email_Titular', max_length=100, description='Cardholder e-mail; gets a summary.'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _CardFields(BaseModel):
|
|
153
|
+
"""Card data, inSite token and 3-D Secure data, accepted by both connections."""
|
|
154
|
+
|
|
155
|
+
pan: str | None = Field(None, alias='Ds_Merchant_Pan', max_length=19, pattern=r'^[0-9]{12,19}$')
|
|
156
|
+
expiry_date: str | None = Field(None, alias='Ds_Merchant_ExpiryDate', pattern=r'^[0-9]{4}$', description='YYMM.')
|
|
157
|
+
cvv2: str | None = Field(None, alias='Ds_Merchant_Cvv2', pattern=r'^[0-9]{3,4}$')
|
|
158
|
+
idoper: str | None = Field(None, alias='Ds_Merchant_Idoper', max_length=40, description='inSite operation token.')
|
|
159
|
+
emv3ds: dict[str, Any] | None = Field(None, alias='Ds_Merchant_EMV3DS', description='Build with redsys.threeds.')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _DirectOperationFields(BaseModel):
|
|
163
|
+
"""Fields of every direct-connection operation."""
|
|
164
|
+
|
|
165
|
+
merchant_descriptor: str | None = Field(
|
|
166
|
+
None, alias='Ds_Merchant_MerchantDescriptor', max_length=25, description='Soft descriptor; bank activation.'
|
|
167
|
+
)
|
|
168
|
+
client_ip: str | None = Field(None, alias='Ds_Merchant_ClientIP', max_length=15, description='Customer IP.')
|
|
134
169
|
|
|
135
170
|
|
|
136
|
-
class _RedirectFields(_StoredCredentialFields):
|
|
171
|
+
class _RedirectFields(_StoredCredentialFields, _CardFields):
|
|
137
172
|
"""Fields of the redirect connection (browser form)."""
|
|
138
173
|
|
|
139
174
|
merchant_url: str | None = Field(None, alias='Ds_Merchant_MerchantURL', max_length=250)
|
|
@@ -143,7 +178,7 @@ class _RedirectFields(_StoredCredentialFields):
|
|
|
143
178
|
product_description: str | None = Field(None, alias='Ds_Merchant_ProductDescription', max_length=125)
|
|
144
179
|
titular: str | None = Field(None, alias='Ds_Merchant_Titular', max_length=60)
|
|
145
180
|
consumer_language: int | None = Field(None, alias='Ds_Merchant_ConsumerLanguage')
|
|
146
|
-
pay_methods: str | None = Field(None, alias='Ds_Merchant_PayMethods', description='
|
|
181
|
+
pay_methods: str | None = Field(None, alias='Ds_Merchant_PayMethods', description='See redsys.api.pay_methods.')
|
|
147
182
|
bizum_mobile_number: str | None = Field(None, alias='Ds_Merchant_Bizum_MobileNumber', max_length=19)
|
|
148
183
|
excep_sca: str | None = Field(
|
|
149
184
|
None,
|
|
@@ -151,32 +186,105 @@ class _RedirectFields(_StoredCredentialFields):
|
|
|
151
186
|
max_length=3,
|
|
152
187
|
description='PSD2 SCA exemption requested; codes in redsys.api.sca.',
|
|
153
188
|
)
|
|
189
|
+
merchant_descriptor: str | None = Field(
|
|
190
|
+
None, alias='Ds_Merchant_MerchantDescriptor', max_length=25, description='Soft descriptor; bank activation.'
|
|
191
|
+
)
|
|
192
|
+
shipping_address_pyp: str | None = Field(
|
|
193
|
+
None, alias='Ds_Merchant_ShippingAddressPyP', pattern=r'^[SN]$', description='See redsys.api.options.'
|
|
194
|
+
)
|
|
195
|
+
perso_code: int | None = Field(
|
|
196
|
+
None, alias='Ds_Merchant_PersoCode', ge=0, le=9, description='Payment page personalization.'
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _XPayFields(BaseModel):
|
|
201
|
+
"""Google Pay and Apple Pay data; the wire names have no MERCHANT infix."""
|
|
202
|
+
|
|
203
|
+
xpay_data: str | None = Field(
|
|
204
|
+
None, alias='DS_XPAYDATA', max_length=10000, description='Encrypted wallet payload, as the wallet guide says.'
|
|
205
|
+
)
|
|
206
|
+
xpay_decoded_data: dict[str, Any] | None = Field(
|
|
207
|
+
None, alias='DS_XPAYDECODEDDATA', description='Wallet payload decrypted by the merchant.'
|
|
208
|
+
)
|
|
209
|
+
xpay_type: str | None = Field(None, alias='DS_XPAYTYPE', description='See redsys.api.xpay.')
|
|
210
|
+
xpay_origen: str | None = Field(None, alias='DS_XPAYORIGEN', description='See redsys.api.xpay.')
|
|
211
|
+
|
|
212
|
+
@field_validator('xpay_type')
|
|
213
|
+
@classmethod
|
|
214
|
+
def _validate_xpay_type(cls, value: str | None) -> str | None:
|
|
215
|
+
if value is not None and value not in xpay.TYPES:
|
|
216
|
+
raise ValueError('xpay_type is not valid.')
|
|
217
|
+
return value
|
|
218
|
+
|
|
219
|
+
@field_validator('xpay_origen')
|
|
220
|
+
@classmethod
|
|
221
|
+
def _validate_xpay_origen(cls, value: str | None) -> str | None:
|
|
222
|
+
if value is not None and value not in xpay.ORIGINS:
|
|
223
|
+
raise ValueError('xpay_origen is not valid.')
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
@model_validator(mode='after')
|
|
227
|
+
def _one_wallet_payload_with_its_type_and_origin(self):
|
|
228
|
+
payloads = (self.xpay_data is not None) + (self.xpay_decoded_data is not None)
|
|
229
|
+
described = (self.xpay_type is not None) + (self.xpay_origen is not None)
|
|
230
|
+
if payloads > 1:
|
|
231
|
+
raise ValueError('xpay_data and xpay_decoded_data are mutually exclusive.')
|
|
232
|
+
if (payloads, described) not in ((0, 0), (1, 2)):
|
|
233
|
+
raise ValueError('a wallet payload goes with xpay_type and xpay_origen.')
|
|
234
|
+
return self
|
|
154
235
|
|
|
155
236
|
|
|
156
|
-
class _DirectFields(_StoredCredentialFields):
|
|
237
|
+
class _DirectFields(_StoredCredentialFields, _CardFields, _DirectOperationFields, _XPayFields):
|
|
157
238
|
"""Fields of the direct connection (server-to-server card data, 3DS, stored credentials)."""
|
|
158
239
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
240
|
+
mpi_external: dict[str, Any] | None = Field(
|
|
241
|
+
None, alias='Ds_Merchant_MpiExternal', description='Results of an external 3DS server.'
|
|
242
|
+
)
|
|
243
|
+
token_data: dict[str, Any] | None = Field(
|
|
244
|
+
None, alias='Ds_Merchant_TokenData', description='Network token cryptogram data.'
|
|
245
|
+
)
|
|
164
246
|
# RedSys spells the wire name "Frecuency".
|
|
165
247
|
date_frequency: str | None = Field(None, alias='Ds_Merchant_DateFrecuency', description='Days between charges.')
|
|
166
248
|
charge_expiry_date: str | None = Field(None, alias='Ds_Merchant_ChargeExpiryDate', description='yyyy-MM-dd.')
|
|
167
249
|
dcc: str | dict[str, Any] | None = Field(
|
|
168
|
-
None,
|
|
250
|
+
None,
|
|
251
|
+
alias='Ds_Merchant_DCC',
|
|
252
|
+
description='options.DCC_QUERY on the 3DS initialization; then the accepted offer.',
|
|
169
253
|
)
|
|
170
254
|
excep_sca: str | None = Field(
|
|
171
255
|
None,
|
|
172
256
|
alias='Ds_Merchant_Excep_SCA',
|
|
173
257
|
max_length=3,
|
|
174
|
-
description='PSD2 SCA exemption requested; codes in redsys.api.sca.
|
|
258
|
+
description='PSD2 SCA exemption requested; codes in redsys.api.sca. sca.QUERY on the 3DS initialization asks.',
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class _PaymentFields(BaseModel):
|
|
263
|
+
"""Fields of payments (type 0) and authentications (type 7) on both connections."""
|
|
264
|
+
|
|
265
|
+
tax_reference: str | None = Field(
|
|
266
|
+
None, alias='Ds_Merchant_Tax_Reference', max_length=100, description='Tax payment barcode; bank activation.'
|
|
175
267
|
)
|
|
268
|
+
ota: str | None = Field(None, alias='Ds_Merchant_OTA', pattern=r'^S$', description='See redsys.api.options.')
|
|
269
|
+
|
|
176
270
|
|
|
271
|
+
class _BizumFields(BaseModel):
|
|
272
|
+
"""Bizum fields of payments on both connections."""
|
|
273
|
+
|
|
274
|
+
bizum_mrt_amount_variation: str | None = Field(
|
|
275
|
+
None,
|
|
276
|
+
alias='Ds_Merchant_Bizum_MrtAmountVariation',
|
|
277
|
+
pattern=r'^[0-9]+$',
|
|
278
|
+
description='Ceiling for later MIT amounts, in minor units; on the operation that creates the TID.',
|
|
279
|
+
)
|
|
177
280
|
|
|
178
|
-
|
|
179
|
-
|
|
281
|
+
|
|
282
|
+
class RedirectPaymentMessage(BaseRequest, _RedirectFields, _PaymentFields, _BizumFields):
|
|
283
|
+
"""Redirect-connection payment (transaction type 0), authentication (7) or refund without original (34)."""
|
|
284
|
+
|
|
285
|
+
bizum_id_oper: str | None = Field(
|
|
286
|
+
None, alias='Ds_Merchant_Bizum_IdOper', description='Response.bizum_id_oper; asks RedSys for the backup.'
|
|
287
|
+
)
|
|
180
288
|
|
|
181
289
|
|
|
182
290
|
class RedirectPreauthorizationMessage(BaseRequest, _RedirectFields):
|
|
@@ -185,12 +293,90 @@ class RedirectPreauthorizationMessage(BaseRequest, _RedirectFields):
|
|
|
185
293
|
transaction_type: int = Field(1, alias='Ds_Merchant_TransactionType')
|
|
186
294
|
|
|
187
295
|
|
|
188
|
-
class
|
|
189
|
-
"""
|
|
296
|
+
class _PayGoldFields(BaseModel):
|
|
297
|
+
"""Fields of a PayGold link (types 15 and 51)."""
|
|
298
|
+
|
|
299
|
+
customer_mobile: str | None = Field(None, alias='Ds_Merchant_Customer_Mobile', max_length=19)
|
|
300
|
+
customer_mail: str | None = Field(None, alias='Ds_Merchant_Customer_Mail', max_length=100)
|
|
301
|
+
customer_sms_text: str | None = Field(
|
|
302
|
+
None, alias='Ds_Merchant_Customer_Sms_Text', max_length=160, description='Must contain paygold.SMS_URL.'
|
|
303
|
+
)
|
|
304
|
+
p2f_expiry_date: str | None = Field(
|
|
305
|
+
None,
|
|
306
|
+
alias='Ds_Merchant_P2f_ExpiryDate',
|
|
307
|
+
pattern=r'^([0-9]{1,23}|[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.[0-9]{2}\.[0-9]{2}\.[0-9]{3})$',
|
|
308
|
+
description='Minutes from now, or yyyy-mm-dd-HH.MM.ss.sss.',
|
|
309
|
+
)
|
|
310
|
+
p2f_xml_data: str | None = Field(
|
|
311
|
+
None, alias='Ds_Merchant_P2f_XmlData', max_length=1000, description='E-mail tags; see redsys.api.paygold.'
|
|
312
|
+
)
|
|
313
|
+
alt_transaction_type: int | None = Field(
|
|
314
|
+
None,
|
|
315
|
+
alias='Ds_Merchant_AltTransType',
|
|
316
|
+
description='transactions.REFUND_WITHOUT_ORIGINAL turns the link into a refund without original.',
|
|
317
|
+
)
|
|
318
|
+
pay_methods: str | None = Field(None, alias='Ds_Merchant_PayMethods', description='See redsys.api.pay_methods.')
|
|
319
|
+
merchant_url: str | None = Field(None, alias='Ds_Merchant_MerchantURL', max_length=250)
|
|
320
|
+
|
|
321
|
+
@field_validator('customer_sms_text')
|
|
322
|
+
@classmethod
|
|
323
|
+
def _sms_text_holds_the_link(cls, value: str | None) -> str | None:
|
|
324
|
+
if value is not None and paygold.SMS_URL not in value:
|
|
325
|
+
raise ValueError(f'customer_sms_text must contain {paygold.SMS_URL}.')
|
|
326
|
+
return value
|
|
327
|
+
|
|
328
|
+
@field_validator('p2f_expiry_date', mode='before')
|
|
329
|
+
@classmethod
|
|
330
|
+
def _minutes_as_text(cls, value):
|
|
331
|
+
return str(value) if isinstance(value, int) and not isinstance(value, bool) else value
|
|
332
|
+
|
|
333
|
+
@field_validator('alt_transaction_type')
|
|
334
|
+
@classmethod
|
|
335
|
+
def _validate_alt_transaction_type(cls, value: int | None) -> int | None:
|
|
336
|
+
if value is not None and value != transactions.REFUND_WITHOUT_ORIGINAL:
|
|
337
|
+
raise ValueError('alt_transaction_type is 34, refund without original.')
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class DirectPaymentMessage(BaseRequest, _DirectFields, _PaymentFields, _PayGoldFields, _BizumFields):
|
|
342
|
+
"""Direct-connection card payment (transaction type 0), and the types without their own message."""
|
|
190
343
|
|
|
191
344
|
sum_total: Decimal | None = Field(
|
|
192
345
|
None, alias='Ds_Merchant_SumTotal', description='Recurring series total; scaled like amount.'
|
|
193
346
|
)
|
|
347
|
+
financing: str | dict[str, Any] | None = Field(
|
|
348
|
+
None,
|
|
349
|
+
alias='Ds_Merchant_Financing',
|
|
350
|
+
description='options.FINANCING_QUERY on the 3DS initialization; then the chosen option.',
|
|
351
|
+
)
|
|
352
|
+
matching_data: str | None = Field(
|
|
353
|
+
None, alias='Ds_Merchant_MatchingData', max_length=21, description='Unique; type 17, with acquirer_identifier.'
|
|
354
|
+
)
|
|
355
|
+
acquirer_identifier: str | None = Field(
|
|
356
|
+
None, alias='Ds_Acquirer_Identifier', pattern=r'^[0-9]{1,6}$', description='Type 17, with matching_data.'
|
|
357
|
+
)
|
|
358
|
+
bizum_mobile_number: str | None = Field(None, alias='Ds_Merchant_Bizum_MobileNumber', max_length=19)
|
|
359
|
+
otp: str | None = Field(
|
|
360
|
+
None, alias='Ds_Merchant_Otp', pattern=r'^[0-9]+$', description='Bizum backup one-time password.'
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@field_validator('otp', mode='before')
|
|
364
|
+
@classmethod
|
|
365
|
+
def _otp_as_text(cls, value):
|
|
366
|
+
return str(value) if isinstance(value, int) and not isinstance(value, bool) else value
|
|
367
|
+
|
|
368
|
+
@field_validator('financing')
|
|
369
|
+
@classmethod
|
|
370
|
+
def _validate_financing(cls, value):
|
|
371
|
+
if isinstance(value, str) and value != options.FINANCING_QUERY:
|
|
372
|
+
raise ValueError('financing is "Y" or the chosen option.')
|
|
373
|
+
return value
|
|
374
|
+
|
|
375
|
+
@model_validator(mode='after')
|
|
376
|
+
def _matching_data_with_acquirer(self):
|
|
377
|
+
if (self.matching_data is None) != (self.acquirer_identifier is None):
|
|
378
|
+
raise ValueError('matching_data and acquirer_identifier go together.')
|
|
379
|
+
return self
|
|
194
380
|
|
|
195
381
|
@field_serializer('sum_total')
|
|
196
382
|
def _serialize_sum_total(self, value: Decimal | None) -> int | None:
|
|
@@ -203,21 +389,30 @@ class DirectPreauthorizationMessage(BaseRequest, _DirectFields):
|
|
|
203
389
|
transaction_type: int = Field(1, alias='Ds_Merchant_TransactionType')
|
|
204
390
|
|
|
205
391
|
|
|
206
|
-
class ConfirmationMessage(BaseRequest):
|
|
392
|
+
class ConfirmationMessage(BaseRequest, _DirectOperationFields):
|
|
207
393
|
"""Capture of a previous preauthorization (transaction type 2)."""
|
|
208
394
|
|
|
209
395
|
transaction_type: int = Field(2, alias='Ds_Merchant_TransactionType')
|
|
210
396
|
|
|
211
397
|
|
|
212
|
-
class RefundMessage(BaseRequest):
|
|
398
|
+
class RefundMessage(BaseRequest, _DirectOperationFields):
|
|
213
399
|
"""Refund of a previous payment (transaction type 3)."""
|
|
214
400
|
|
|
215
401
|
transaction_type: int = Field(3, alias='Ds_Merchant_TransactionType')
|
|
216
402
|
authorization_code: str | None = Field(None, alias='Ds_Merchant_AuthorisationCode', max_length=6)
|
|
403
|
+
transaction_date: str | None = Field(
|
|
404
|
+
None,
|
|
405
|
+
alias='Ds_Merchant_TransactionDate',
|
|
406
|
+
pattern=r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$',
|
|
407
|
+
description='yyyy-mm-dd of the original operation.',
|
|
408
|
+
)
|
|
409
|
+
rts: str | None = Field(
|
|
410
|
+
None, alias='Ds_Merchant_RTS', pattern=r'^[0-9]{1,24}$', description='Response.rts of a partial confirmation.'
|
|
411
|
+
)
|
|
217
412
|
|
|
218
413
|
|
|
219
|
-
class CancellationMessage(BaseRequest):
|
|
220
|
-
"""
|
|
414
|
+
class CancellationMessage(BaseRequest, _DirectOperationFields):
|
|
415
|
+
"""Void of a preauthorization (transaction type 9) or another cancellation (types 45 to 47)."""
|
|
221
416
|
|
|
222
417
|
transaction_type: int = Field(9, alias='Ds_Merchant_TransactionType')
|
|
223
418
|
|
|
@@ -247,8 +442,7 @@ class Response(BaseModel):
|
|
|
247
442
|
card_country: str | None = Field(None, alias='Ds_Card_Country')
|
|
248
443
|
card_type: str | None = Field(None, alias='Ds_Card_Type')
|
|
249
444
|
card_brand: str | None = Field(None, alias='Ds_Card_Brand')
|
|
250
|
-
# Not in the integration guides
|
|
251
|
-
# and modeled by the official PHP library 2.0.1.
|
|
445
|
+
# Not in the integration guides; returned by the test environment.
|
|
252
446
|
card_typology: str | None = Field(None, alias='Ds_Card_Typology', description='For example CONSUMO.')
|
|
253
447
|
eci: str | None = Field(None, alias='Ds_ECI', description='E-commerce indicator of the authentication.')
|
|
254
448
|
response_description: str | None = Field(None, alias='Ds_Response_Description')
|
|
@@ -268,11 +462,26 @@ class Response(BaseModel):
|
|
|
268
462
|
dcc: dict[str, Any] | None = Field(None, alias='Ds_DCC', description='Offer from the 3DS initialization.')
|
|
269
463
|
identifier: str | None = Field(None, alias='Ds_Merchant_Identifier', description='Token returned on request.')
|
|
270
464
|
cof_txnid: str | None = Field(None, alias='Ds_Merchant_Cof_Txnid')
|
|
271
|
-
processed_pay_method: str | None = Field(
|
|
465
|
+
processed_pay_method: str | None = Field(
|
|
466
|
+
None, alias='Ds_ProcessedPayMethod', description='See redsys.api.processed_pay_methods.'
|
|
467
|
+
)
|
|
272
468
|
bizum_id_oper: str | None = Field(None, alias='Ds_Bizum_IdOper')
|
|
273
469
|
bizum_cuenta_truncada: str | None = Field(None, alias='Ds_Bizum_CuentaTruncada')
|
|
274
470
|
currency_dcc: str | None = Field(None, alias='Ds_Currency_DCC', description='Dynamic currency conversion.')
|
|
275
471
|
amount_dcc: str | None = Field(None, alias='Ds_Amount_DCC', description='Minor units in Ds_Currency_DCC.')
|
|
472
|
+
rts: str | None = Field(None, alias='rts', description='Partial confirmation reference; refund it with rts=.')
|
|
473
|
+
rtp_status: str | None = Field(None, alias='Ds_RtpStatus', description='Bizum Request-to-Pay: OK or KO.')
|
|
474
|
+
rtp_response: str | None = Field(None, alias='Ds_RtpResponse', description='BIZnnnnn; see redsys.api.bizum.')
|
|
475
|
+
rtp_description: str | None = Field(None, alias='Ds_RtpDescription')
|
|
476
|
+
rtp_backup_html: str | None = Field(None, alias='Ds_RtpBackupHtml', description='Base64 HTML backup form.')
|
|
477
|
+
rtp_backup_template: str | None = Field(
|
|
478
|
+
None, alias='Ds_RtpBackupTemplate', description='Base64 JSON pieces of a backup form.'
|
|
479
|
+
)
|
|
480
|
+
url_pago_2_fases: str | None = Field(None, alias='Ds_UrlPago2Fases', description='PayGold payment link.')
|
|
481
|
+
|
|
482
|
+
_http_status: int | None = PrivateAttr(None)
|
|
483
|
+
_http_headers: dict[str, str] | None = PrivateAttr(None)
|
|
484
|
+
_raw_body: str | None = PrivateAttr(None)
|
|
276
485
|
|
|
277
486
|
@field_validator('amount', mode='before')
|
|
278
487
|
@classmethod
|
|
@@ -292,25 +501,50 @@ class Response(BaseModel):
|
|
|
292
501
|
def get_order(self) -> str | None:
|
|
293
502
|
return self.order
|
|
294
503
|
|
|
504
|
+
def _with_transport(self, http_status: int, http_headers: dict[str, str], raw_body: str) -> 'Response':
|
|
505
|
+
self._http_status, self._http_headers, self._raw_body = http_status, http_headers, raw_body
|
|
506
|
+
return self
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def http_status(self) -> int | None:
|
|
510
|
+
"""HTTP status of the direct call; ``None`` when parsed from a received payload."""
|
|
511
|
+
return self._http_status
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def http_headers(self) -> dict[str, str] | None:
|
|
515
|
+
"""HTTP headers of the direct call; ``None`` when parsed from a received payload."""
|
|
516
|
+
return self._http_headers
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def raw_body(self) -> str | None:
|
|
520
|
+
"""Unprocessed HTTP body of the direct call; ``None`` when parsed from a received payload."""
|
|
521
|
+
return self._raw_body
|
|
522
|
+
|
|
295
523
|
@property
|
|
296
524
|
def response_code(self):
|
|
297
|
-
# A 3DS challenge response may carry no Ds_Response.
|
|
298
|
-
# as-is; see docs/response-codes.md for the English meanings.
|
|
525
|
+
# A 3DS challenge response may carry no Ds_Response.
|
|
299
526
|
return int(self.response) if self.response not in (None, '') else None
|
|
300
527
|
|
|
528
|
+
def describe(self, language='es') -> str:
|
|
529
|
+
"""Meaning of ``Ds_Response``, or of ``Ds_ErrorCode`` without it; see ``redsys.codes.describe``."""
|
|
530
|
+
code = self.response if self.response not in (None, '') else self.error_code
|
|
531
|
+
return describe(code, language) if code else ''
|
|
532
|
+
|
|
301
533
|
def is_paid(self) -> bool:
|
|
302
|
-
return
|
|
534
|
+
return (
|
|
535
|
+
self.response_code is not None and responses.AUTHORIZED <= self.response_code <= responses.AUTHORIZED_LAST
|
|
536
|
+
)
|
|
303
537
|
|
|
304
538
|
def is_refunded(self) -> bool:
|
|
305
|
-
return self.response_code ==
|
|
539
|
+
return self.response_code == responses.REFUND_OR_CONFIRMATION_AUTHORIZED
|
|
306
540
|
|
|
307
541
|
def is_canceled(self) -> bool:
|
|
308
|
-
return self.response_code ==
|
|
542
|
+
return self.response_code == responses.CANCELLATION_AUTHORIZED
|
|
309
543
|
|
|
310
544
|
def is_authorized(self) -> bool:
|
|
311
545
|
return self.is_paid() or self.is_refunded() or self.is_canceled()
|
|
312
546
|
|
|
313
|
-
#
|
|
547
|
+
# == EMV 3DS accessors over Ds_EMV3DS ==
|
|
314
548
|
@property
|
|
315
549
|
def _emv3ds(self) -> dict:
|
|
316
550
|
return self.emv3ds or {}
|
|
@@ -336,10 +570,10 @@ class Response(BaseModel):
|
|
|
336
570
|
return self._emv3ds.get('creq')
|
|
337
571
|
|
|
338
572
|
def is_no_3ds(self) -> bool:
|
|
339
|
-
return self.protocol_version ==
|
|
573
|
+
return self.protocol_version == emv3ds.NO_3DS_V2
|
|
340
574
|
|
|
341
575
|
def requires_challenge(self) -> bool:
|
|
342
|
-
return self._emv3ds.get('threeDSInfo') ==
|
|
576
|
+
return self._emv3ds.get('threeDSInfo') == emv3ds.CHALLENGE_REQUEST
|
|
343
577
|
|
|
344
578
|
|
|
345
579
|
__all__ = [
|
redsys/orders.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Order numbers for ``Ds_Merchant_Order``."""
|
|
2
|
+
|
|
3
|
+
from secrets import choice
|
|
4
|
+
from string import ascii_letters, digits
|
|
5
|
+
|
|
6
|
+
_ALPHANUMERIC = ascii_letters + digits
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_order(length: int = 12) -> str:
|
|
10
|
+
"""A random order number of ``length`` characters (4 to 12): 4 digits, then letters and digits.
|
|
11
|
+
|
|
12
|
+
RedSys recommends the 4 leading digits. A random draw makes a repeated order (SIS0051)
|
|
13
|
+
unlikely, not impossible. Raises ``ValueError`` for a length outside 4 to 12.
|
|
14
|
+
"""
|
|
15
|
+
if isinstance(length, bool) or not isinstance(length, int) or not 4 <= length <= 12:
|
|
16
|
+
raise ValueError('length must be an integer from 4 to 12.')
|
|
17
|
+
return ''.join(choice(digits) for _ in range(4)) + ''.join(choice(_ALPHANUMERIC) for _ in range(length - 4))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ['generate_order']
|
redsys/request.py
CHANGED
|
@@ -9,26 +9,32 @@ def _as_text(value):
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Request:
|
|
12
|
-
"""A signed request,
|
|
12
|
+
"""A signed request, the return of every operation.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- ``to_dict()`` -- the three ``Ds_*`` fields to POST (redirect form fields or
|
|
19
|
-
REST JSON body). Always available.
|
|
20
|
-
- ``send()`` -- performs the REST call and returns the parsed ``Response``.
|
|
21
|
-
Only direct-connection clients bind a sender; for redirect requests (the
|
|
22
|
-
browser submits them) it raises ``TypeError`` -- use ``to_dict()``.
|
|
23
|
-
|
|
24
|
-
Async clients bind an awaitable sender, so ``await request.send()`` works.
|
|
14
|
+
``to_dict()`` gives the three ``Ds_*`` fields to post: the redirect form fields or the
|
|
15
|
+
REST JSON body. ``send()`` performs the REST call and returns the parsed ``Response``;
|
|
16
|
+
it is awaitable on ``AsyncDirectClient`` and raises ``TypeError`` on a redirect request.
|
|
25
17
|
"""
|
|
26
18
|
|
|
27
|
-
def __init__(
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
merchant_parameters: bytes,
|
|
22
|
+
signature: bytes,
|
|
23
|
+
signature_version: str,
|
|
24
|
+
*,
|
|
25
|
+
sender=None,
|
|
26
|
+
endpoint: str | None = None,
|
|
27
|
+
):
|
|
28
28
|
self._merchant_parameters = merchant_parameters
|
|
29
29
|
self._signature = signature
|
|
30
30
|
self._signature_version = signature_version
|
|
31
31
|
self._sender = sender
|
|
32
|
+
self._endpoint = endpoint
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def endpoint(self) -> str | None:
|
|
36
|
+
"""The URL this request goes to: the form ``action`` on redirect, the REST URL on direct."""
|
|
37
|
+
return self._endpoint
|
|
32
38
|
|
|
33
39
|
@property
|
|
34
40
|
def merchant_parameters(self) -> bytes:
|
redsys/threeds.py
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
"""Builders for the Ds_Merchant_EMV3DS object
|
|
1
|
+
"""Builders for the ``Ds_Merchant_EMV3DS`` object of 3-D Secure v2 on the direct connection.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
finalize (`challenge_response`). See docs/direct-connection.md. The
|
|
5
|
-
EMV3DS sub-field names are case-sensitive and passed through verbatim.
|
|
3
|
+
The step numbers follow docs/direct-connection.md. The sub-field names are case-sensitive.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
client.payment(emv3ds=threeds.authentication_data(..., three_ds_comp_ind=emv3ds.METHOD_COMPLETED), ...)
|
|
6
|
+
"""
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
from base64 import urlsafe_b64encode
|
|
9
|
+
from json import dumps
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
"""
|
|
11
|
+
from redsys.api import emv3ds
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def card_data() -> dict:
|
|
16
|
-
"""
|
|
17
|
-
return {'threeDSInfo':
|
|
15
|
+
"""``emv3ds`` of the 3-D Secure initialization, iniciaPeticionREST (step 1)."""
|
|
16
|
+
return {'threeDSInfo': emv3ds.CARD_DATA}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def method_data(*, three_ds_server_trans_id, notification_url) -> str:
|
|
20
|
+
"""Value of the ``threeDSMethodData`` form field the browser posts to ``threeDSMethodURL`` (step 2)."""
|
|
21
|
+
payload = {'threeDSServerTransID': three_ds_server_trans_id, 'threeDSMethodNotificationURL': notification_url}
|
|
22
|
+
return urlsafe_b64encode(dumps(payload, separators=(',', ':')).encode('utf-8')).decode('ascii').rstrip('=')
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
def browser_info(
|
|
@@ -30,7 +35,7 @@ def browser_info(
|
|
|
30
35
|
javascript_enabled=True,
|
|
31
36
|
ip=None,
|
|
32
37
|
) -> dict:
|
|
33
|
-
"""Cardholder browser data the merchant page
|
|
38
|
+
"""Cardholder browser data, collected by the merchant page, for ``authentication_data()``."""
|
|
34
39
|
data = {
|
|
35
40
|
'browserAcceptHeader': accept_header,
|
|
36
41
|
'browserUserAgent': user_agent,
|
|
@@ -48,11 +53,11 @@ def browser_info(
|
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
def authentication_data(
|
|
51
|
-
*, three_ds_server_trans_id, notification_url, protocol_version, browser, three_ds_comp_ind=
|
|
56
|
+
*, three_ds_server_trans_id, notification_url, protocol_version, browser, three_ds_comp_ind=emv3ds.METHOD_COMPLETED
|
|
52
57
|
) -> dict:
|
|
53
|
-
"""
|
|
58
|
+
"""``emv3ds`` of the authentication (step 3); ``browser`` is ``browser_info(...)``."""
|
|
54
59
|
data = {
|
|
55
|
-
'threeDSInfo':
|
|
60
|
+
'threeDSInfo': emv3ds.AUTHENTICATION_DATA,
|
|
56
61
|
'protocolVersion': protocol_version,
|
|
57
62
|
'threeDSServerTransID': three_ds_server_trans_id,
|
|
58
63
|
'notificationURL': notification_url,
|
|
@@ -63,5 +68,5 @@ def authentication_data(
|
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
def challenge_response(*, cres, protocol_version) -> dict:
|
|
66
|
-
"""
|
|
67
|
-
return {'threeDSInfo':
|
|
71
|
+
"""``emv3ds`` of the operation after the challenge (step 5)."""
|
|
72
|
+
return {'threeDSInfo': emv3ds.CHALLENGE_RESPONSE, 'protocolVersion': protocol_version, 'cres': cres}
|