python-redsys 2.0.0__tar.gz → 2.1.0__tar.gz
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/python_redsys.egg-info → python_redsys-2.1.0}/PKG-INFO +13 -2
- {python_redsys-2.0.0 → python_redsys-2.1.0}/README.md +12 -1
- {python_redsys-2.0.0 → python_redsys-2.1.0}/pyproject.toml +6 -2
- {python_redsys-2.0.0 → python_redsys-2.1.0/python_redsys.egg-info}/PKG-INFO +13 -2
- python_redsys-2.1.0/redsys/api/transactions.py +74 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/clients.py +5 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/models.py +32 -14
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_direct.py +23 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_live.py +31 -6
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_models.py +26 -2
- python_redsys-2.0.0/redsys/api/transactions.py +0 -46
- {python_redsys-2.0.0 → python_redsys-2.1.0}/LICENSE +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/NOTICE +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/python_redsys.egg-info/SOURCES.txt +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/python_redsys.egg-info/dependency_links.txt +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/python_redsys.egg-info/requires.txt +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/python_redsys.egg-info/top_level.txt +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/__init__.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/api/__init__.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/api/cof.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/api/currencies.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/api/languages.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/api/sca.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/crypto.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/exceptions.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/py.typed +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/request.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/redsys/threeds.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/setup.cfg +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_clients.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_crypto.py +0 -0
- {python_redsys-2.0.0 → python_redsys-2.1.0}/tests/test_threeds.py +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
|
|
|
@@ -19,6 +19,17 @@ pip install "python-redsys[http]"
|
|
|
19
19
|
|
|
20
20
|
Requires Python 3.10+. Runtime dependencies: `cryptography` and `pydantic` (v2).
|
|
21
21
|
|
|
22
|
+
## Status
|
|
23
|
+
|
|
24
|
+
| | |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Version | 2.1.0; 1.x receives fixes until 2026-12-31 |
|
|
27
|
+
| Python | 3.10 to 3.15 |
|
|
28
|
+
| 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) |
|
|
29
|
+
| Signature versions | `HMAC_SHA512_V2` (default), `HMAC_SHA512_V1`, `HMAC_SHA256_V1` |
|
|
30
|
+
| 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 |
|
|
31
|
+
| Not yet verified live | Frictionless 3DS and the 3DSMethod step |
|
|
32
|
+
|
|
22
33
|
## Upgrading from 1.x
|
|
23
34
|
|
|
24
35
|
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.
|
|
@@ -165,7 +176,7 @@ client.payment(
|
|
|
165
176
|
).send()
|
|
166
177
|
```
|
|
167
178
|
|
|
168
|
-
`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
|
|
179
|
+
`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`.
|
|
169
180
|
|
|
170
181
|
## Signature versions
|
|
171
182
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-redsys"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "A simple, lightweight client to handle payments through the RedSys platform."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -40,7 +40,8 @@ lint = ["ruff", "mypy"]
|
|
|
40
40
|
dev = [
|
|
41
41
|
{ include-group = "test" },
|
|
42
42
|
{ include-group = "lint" },
|
|
43
|
-
|
|
43
|
+
# filelock releases with the 2026 security fixes need Python 3.11; pre-commit pulls it in.
|
|
44
|
+
"pre-commit; python_version >= '3.11'",
|
|
44
45
|
]
|
|
45
46
|
|
|
46
47
|
[tool.setuptools.packages.find]
|
|
@@ -60,6 +61,9 @@ select = ["E", "F", "I", "B", "UP"]
|
|
|
60
61
|
[tool.ruff.format]
|
|
61
62
|
quote-style = "single"
|
|
62
63
|
|
|
64
|
+
[tool.coverage.report]
|
|
65
|
+
fail_under = 95
|
|
66
|
+
|
|
63
67
|
[tool.mypy]
|
|
64
68
|
python_version = "3.10"
|
|
65
69
|
ignore_missing_imports = true
|
|
@@ -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
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
AUTHORIZATION = 0
|
|
2
|
+
"""Authorization."""
|
|
3
|
+
|
|
4
|
+
PREAUTHORIZATION = 1
|
|
5
|
+
"""Pre-authorization."""
|
|
6
|
+
|
|
7
|
+
CONFIRMATION = 2
|
|
8
|
+
"""Confirmation."""
|
|
9
|
+
|
|
10
|
+
REFUND = 3
|
|
11
|
+
"""Refund."""
|
|
12
|
+
|
|
13
|
+
AUTHENTICATION = 7
|
|
14
|
+
"""Authentication."""
|
|
15
|
+
|
|
16
|
+
AUTHENTICATION_CONFIRMATION = 8
|
|
17
|
+
"""Authentication confirmation"""
|
|
18
|
+
|
|
19
|
+
CANCELLATION = 9
|
|
20
|
+
"""Cancellation"""
|
|
21
|
+
|
|
22
|
+
REPLACEMENT_PREAUTHORIZATION = 11
|
|
23
|
+
"""Replacement preauthorization: extends an existing preauthorization."""
|
|
24
|
+
|
|
25
|
+
PAY_GOLD = 15
|
|
26
|
+
"""Pay gold"""
|
|
27
|
+
|
|
28
|
+
AUTHENTICATION_PUCE = 17
|
|
29
|
+
"""PUCE authentication"""
|
|
30
|
+
|
|
31
|
+
REFUND_WITHOUT_ORIGINAL = 34
|
|
32
|
+
"""Refund without original"""
|
|
33
|
+
|
|
34
|
+
BETTING_PRICE = 37
|
|
35
|
+
"""Betting price"""
|
|
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
|
+
|
|
55
|
+
TRANSACTION_TYPES = (
|
|
56
|
+
AUTHORIZATION,
|
|
57
|
+
PREAUTHORIZATION,
|
|
58
|
+
CONFIRMATION,
|
|
59
|
+
REFUND,
|
|
60
|
+
AUTHENTICATION,
|
|
61
|
+
AUTHENTICATION_CONFIRMATION,
|
|
62
|
+
CANCELLATION,
|
|
63
|
+
REPLACEMENT_PREAUTHORIZATION,
|
|
64
|
+
PAY_GOLD,
|
|
65
|
+
AUTHENTICATION_PUCE,
|
|
66
|
+
REFUND_WITHOUT_ORIGINAL,
|
|
67
|
+
BETTING_PRICE,
|
|
68
|
+
PAY_GOLD_AUTHORIZATION,
|
|
69
|
+
DELETE_REFERENCE,
|
|
70
|
+
PAYMENT_CANCELLATION,
|
|
71
|
+
REFUND_CANCELLATION,
|
|
72
|
+
AUTHENTICATION_CONFIRMATION_CANCELLATION,
|
|
73
|
+
PARTIAL_CONFIRMATION,
|
|
74
|
+
)
|
|
@@ -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
|
|
@@ -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
|
]
|
|
@@ -35,6 +35,12 @@ def _client(cls=DirectClient, **overrides):
|
|
|
35
35
|
return cls(SECRET, **params)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _decoded_params(request):
|
|
39
|
+
from redsys.models import decode_merchant_parameters
|
|
40
|
+
|
|
41
|
+
return decode_merchant_parameters(request.merchant_parameters)
|
|
42
|
+
|
|
43
|
+
|
|
38
44
|
def _payment(client):
|
|
39
45
|
return client.payment(
|
|
40
46
|
order=ORDER,
|
|
@@ -169,3 +175,20 @@ def test_async_inicia_posts_and_parses():
|
|
|
169
175
|
assert response.is_paid()
|
|
170
176
|
assert captured['url'] == 'https://sis-t.redsys.es:25443/sis/rest/iniciaPeticionREST'
|
|
171
177
|
assert set(captured['json']) == {SIGNATURE_VERSION, MERCHANT_PARAMETERS, SIGNATURE}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_extra_transaction_types_build_on_their_message():
|
|
181
|
+
from redsys import transactions
|
|
182
|
+
|
|
183
|
+
client = DirectClient('sq7HjrUOBfKmC576ILgskD5srU870gJ7', merchant_code='999008881', terminal=1, sandbox=True)
|
|
184
|
+
for code in (11, 44, 45, 46, 47, 48):
|
|
185
|
+
assert code in transactions.TRANSACTION_TYPES
|
|
186
|
+
request = client.request(code, order='000000000001', amount=Decimal('1.00'), currency=978)
|
|
187
|
+
assert _decoded_params(request)['Ds_Merchant_TransactionType'] == code
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_dcc_passes_through_request_and_response():
|
|
191
|
+
client = DirectClient('sq7HjrUOBfKmC576ILgskD5srU870gJ7', merchant_code='999008881', terminal=1, sandbox=True)
|
|
192
|
+
offer = {'monedaDCC': '840', 'importeDCC': '117'}
|
|
193
|
+
request = client.payment(order='000000000001', amount=Decimal('1.00'), currency=978, dcc=offer)
|
|
194
|
+
assert _decoded_params(request)['Ds_Merchant_DCC'] == offer
|
|
@@ -16,7 +16,7 @@ import pytest
|
|
|
16
16
|
pytestmark = pytest.mark.skipif(os.environ.get('REDSYS_LIVE') != '1', reason='set REDSYS_LIVE=1 to run')
|
|
17
17
|
httpx = pytest.importorskip('httpx')
|
|
18
18
|
|
|
19
|
-
from redsys import currencies, threeds # noqa: E402
|
|
19
|
+
from redsys import cof, currencies, sca, threeds # noqa: E402
|
|
20
20
|
from redsys.clients import HMAC_SHA256_V1, HMAC_SHA512_V1, HMAC_SHA512_V2, DirectClient # noqa: E402
|
|
21
21
|
|
|
22
22
|
SECRET = 'sq7HjrUOBfKmC576ILgskD5srU870gJ7'
|
|
@@ -68,9 +68,8 @@ def test_live_payment_without_authentication_requires_sca(version):
|
|
|
68
68
|
assert response.response_code == 195
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
client, operation = _client(version), _operation()
|
|
71
|
+
def _challenged_payment(client, operation, **fields):
|
|
72
|
+
"""Initialization, authentication with the simulator's challenge, and the final authorization."""
|
|
74
73
|
card_config = client.inicia(emv3ds=threeds.card_data(), **operation, **CARD).send()
|
|
75
74
|
authentication = threeds.authentication_data(
|
|
76
75
|
three_ds_server_trans_id=card_config.three_ds_server_trans_id,
|
|
@@ -79,13 +78,39 @@ def test_live_challenged_payment_then_refund(version):
|
|
|
79
78
|
browser=BROWSER,
|
|
80
79
|
three_ds_comp_ind='N',
|
|
81
80
|
)
|
|
82
|
-
challenge = client.payment(emv3ds=authentication, **operation, **CARD).send()
|
|
81
|
+
challenge = client.payment(emv3ds=authentication, **operation, **CARD, **fields).send()
|
|
83
82
|
assert challenge.requires_challenge()
|
|
84
83
|
|
|
85
84
|
cres = _answer_simulated_challenge(challenge)
|
|
86
85
|
final = threeds.challenge_response(cres=cres, protocol_version=challenge.protocol_version)
|
|
87
|
-
|
|
86
|
+
return client.payment(emv3ds=final, **operation, **CARD, **fields).send()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize('version', VERSIONS)
|
|
90
|
+
def test_live_challenged_payment_then_refund(version):
|
|
91
|
+
client, operation = _client(version), _operation()
|
|
92
|
+
payment = _challenged_payment(client, operation)
|
|
88
93
|
assert payment.is_paid() and payment.authorization_code
|
|
89
94
|
|
|
90
95
|
refund = client.refund(**operation).send()
|
|
91
96
|
assert refund.is_refunded()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize('version', VERSIONS)
|
|
100
|
+
def test_live_token_then_merchant_initiated_charge(version):
|
|
101
|
+
client = _client(version)
|
|
102
|
+
first = _challenged_payment(
|
|
103
|
+
client, _operation(), identifier=cof.REQUEST_TOKEN, cof_ini=cof.INITIAL, cof_type=cof.RECURRING
|
|
104
|
+
)
|
|
105
|
+
assert first.is_paid() and first.identifier and first.cof_txnid
|
|
106
|
+
|
|
107
|
+
charge = client.payment(
|
|
108
|
+
identifier=first.identifier,
|
|
109
|
+
cof_ini=cof.SUBSEQUENT,
|
|
110
|
+
cof_type=cof.RECURRING,
|
|
111
|
+
cof_txnid=first.cof_txnid,
|
|
112
|
+
excep_sca=sca.MIT,
|
|
113
|
+
direct_payment='true',
|
|
114
|
+
**_operation(),
|
|
115
|
+
).send()
|
|
116
|
+
assert charge.is_paid()
|
|
@@ -158,9 +158,9 @@ def test_response_predicates_and_code():
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def test_response_keeps_unmapped_fields():
|
|
161
|
-
resp = _response(Ds_Response='0000', Ds_Order='1', Ds_Amount='100',
|
|
161
|
+
resp = _response(Ds_Response='0000', Ds_Order='1', Ds_Amount='100', Ds_Control_1790968949956='1790968949956')
|
|
162
162
|
assert resp.is_paid()
|
|
163
|
-
assert resp.model_extra == {'
|
|
163
|
+
assert resp.model_extra == {'Ds_Control_1790968949956': '1790968949956'}
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
def test_response_amount_euro_and_processed_pay_method():
|
|
@@ -195,6 +195,9 @@ def test_request_amount_scaled_by_currency_exponent():
|
|
|
195
195
|
def test_response_amount_scaled_by_currency_exponent():
|
|
196
196
|
assert _response(Ds_Response='0000', Ds_Order='1', Ds_Amount='1056', Ds_Currency='978').amount == Decimal('10.56')
|
|
197
197
|
assert _response(Ds_Response='0000', Ds_Order='1', Ds_Amount='1000', Ds_Currency='392').amount == Decimal('1000')
|
|
198
|
+
# The currency's decimals are kept, as RedSys shows them: 100 EUR minor units are 1.00.
|
|
199
|
+
assert str(_response(Ds_Response='0000', Ds_Order='1', Ds_Amount='100', Ds_Currency='978').amount) == '1.00'
|
|
200
|
+
assert str(_response(Ds_Response='0000', Ds_Order='1', Ds_Amount='100', Ds_Currency='392').amount) == '100'
|
|
198
201
|
|
|
199
202
|
|
|
200
203
|
def test_response_is_canceled():
|
|
@@ -333,3 +336,24 @@ def test_response_bizum_fields():
|
|
|
333
336
|
)
|
|
334
337
|
assert (resp.processed_pay_method, resp.bizum_id_oper, resp.bizum_cuenta_truncada) == ('68', '9382755', 'XXXX4000')
|
|
335
338
|
assert resp.model_extra == {}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_response_card_number_from_redirect_and_rest_names():
|
|
342
|
+
# Redirect notifications send Ds_Card_Number (seen live 2026-10-25); REST responses send Ds_CardNumber.
|
|
343
|
+
redirect = _response(Ds_Response='0000', Ds_Order='1', Ds_Card_Number='454881******0003')
|
|
344
|
+
rest = _response(Ds_Response='0000', Ds_Order='1', Ds_CardNumber='454881******0003')
|
|
345
|
+
assert redirect.card_number == rest.card_number == '454881******0003'
|
|
346
|
+
assert redirect.model_extra == {}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_response_dcc_object():
|
|
350
|
+
resp = _response(Ds_Order='1', Ds_DCC={'InfoMonedaTarjeta': {'monedaDCC': '840'}})
|
|
351
|
+
assert resp.dcc == {'InfoMonedaTarjeta': {'monedaDCC': '840'}}
|
|
352
|
+
assert resp.model_extra == {}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_redirect_accepts_stored_credential_fields():
|
|
356
|
+
params = _valid(identifier='REQUIRED', cof_ini='S', cof_type='R', direct_payment='true').prepare_parameters()
|
|
357
|
+
assert params['Ds_Merchant_Identifier'] == 'REQUIRED'
|
|
358
|
+
assert (params['Ds_Merchant_Cof_Ini'], params['Ds_Merchant_Cof_Type']) == ('S', 'R')
|
|
359
|
+
assert params['Ds_Merchant_DirectPayment'] == 'true'
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
AUTHORIZATION = 0
|
|
2
|
-
"""Authorization."""
|
|
3
|
-
|
|
4
|
-
PREAUTHORIZATION = 1
|
|
5
|
-
"""Pre-authorization."""
|
|
6
|
-
|
|
7
|
-
CONFIRMATION = 2
|
|
8
|
-
"""Confirmation."""
|
|
9
|
-
|
|
10
|
-
REFUND = 3
|
|
11
|
-
"""Refund."""
|
|
12
|
-
|
|
13
|
-
AUTHENTICATION = 7
|
|
14
|
-
"""Authentication."""
|
|
15
|
-
|
|
16
|
-
AUTHENTICATION_CONFIRMATION = 8
|
|
17
|
-
"""Authentication confirmation"""
|
|
18
|
-
|
|
19
|
-
CANCELLATION = 9
|
|
20
|
-
"""Cancellation"""
|
|
21
|
-
|
|
22
|
-
PAY_GOLD = 15
|
|
23
|
-
"""Pay gold"""
|
|
24
|
-
|
|
25
|
-
AUTHENTICATION_PUCE = 17
|
|
26
|
-
"""PUCE authentication"""
|
|
27
|
-
|
|
28
|
-
REFUND_WITHOUT_ORIGINAL = 34
|
|
29
|
-
"""Refund without original"""
|
|
30
|
-
|
|
31
|
-
BETTING_PRICE = 37
|
|
32
|
-
"""Betting price"""
|
|
33
|
-
|
|
34
|
-
TRANSACTION_TYPES = (
|
|
35
|
-
AUTHORIZATION,
|
|
36
|
-
PREAUTHORIZATION,
|
|
37
|
-
CONFIRMATION,
|
|
38
|
-
REFUND,
|
|
39
|
-
AUTHENTICATION,
|
|
40
|
-
AUTHENTICATION_CONFIRMATION,
|
|
41
|
-
CANCELLATION,
|
|
42
|
-
PAY_GOLD,
|
|
43
|
-
AUTHENTICATION_PUCE,
|
|
44
|
-
REFUND_WITHOUT_ORIGINAL,
|
|
45
|
-
BETTING_PRICE,
|
|
46
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|