django-oscar-redsys 0.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.
- django_oscar_redsys-0.1.0/.gitignore +9 -0
- django_oscar_redsys-0.1.0/.python-version +1 -0
- django_oscar_redsys-0.1.0/LICENSE +29 -0
- django_oscar_redsys-0.1.0/PKG-INFO +247 -0
- django_oscar_redsys-0.1.0/README.md +220 -0
- django_oscar_redsys-0.1.0/pyproject.toml +76 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/__init__.py +3 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/admin.py +190 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/apps.py +10 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/conf.py +57 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/emv3ds.py +86 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/facade.py +157 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/language.py +32 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/0001_initial.py +32 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/0002_redsysoperation.py +54 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/__init__.py +0 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/models.py +69 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/order_number.py +24 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/params.py +38 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/response_codes.py +21 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/rest.py +172 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/signals.py +22 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/signature.py +112 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/templates/oscar_redsys/redirect.html +18 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/templates/oscar_redsys/return.html +18 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/transaction_types.py +19 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/urls.py +12 -0
- django_oscar_redsys-0.1.0/src/oscar_redsys/views.py +173 -0
- django_oscar_redsys-0.1.0/tests/__init__.py +0 -0
- django_oscar_redsys-0.1.0/tests/conftest.py +24 -0
- django_oscar_redsys-0.1.0/tests/settings.py +61 -0
- django_oscar_redsys-0.1.0/tests/test_admin.py +220 -0
- django_oscar_redsys-0.1.0/tests/test_conf.py +28 -0
- django_oscar_redsys-0.1.0/tests/test_emv3ds.py +53 -0
- django_oscar_redsys-0.1.0/tests/test_facade.py +241 -0
- django_oscar_redsys-0.1.0/tests/test_language.py +26 -0
- django_oscar_redsys-0.1.0/tests/test_models.py +30 -0
- django_oscar_redsys-0.1.0/tests/test_order_number.py +36 -0
- django_oscar_redsys-0.1.0/tests/test_params.py +66 -0
- django_oscar_redsys-0.1.0/tests/test_response_codes.py +21 -0
- django_oscar_redsys-0.1.0/tests/test_rest.py +122 -0
- django_oscar_redsys-0.1.0/tests/test_signature.py +153 -0
- django_oscar_redsys-0.1.0/tests/test_views.py +179 -0
- django_oscar_redsys-0.1.0/tests/urls.py +7 -0
- django_oscar_redsys-0.1.0/uv.lock +1106 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Diego Cebrian
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-oscar-redsys
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Redsys (TPV Virtual) redirection-method payment integration for django-oscar
|
|
5
|
+
Project-URL: Homepage, https://github.com/hisie/django-oscar-redsys
|
|
6
|
+
Project-URL: Repository, https://github.com/hisie/django-oscar-redsys
|
|
7
|
+
Project-URL: Issues, https://github.com/hisie/django-oscar-redsys/issues
|
|
8
|
+
Author-email: Diego Cebrian <diego@cebrian-it.fr>
|
|
9
|
+
License: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: django,django-oscar,ecommerce,payments,redsys,tpv-virtual
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: django-oscar<4.3,>=4.2
|
|
23
|
+
Requires-Dist: django<5.3,>=5.2
|
|
24
|
+
Requires-Dist: pycryptodome>=3.20
|
|
25
|
+
Requires-Dist: requests>=2.31
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# django-oscar-redsys
|
|
29
|
+
|
|
30
|
+
Redsys (TPV Virtual) payment integration for [django-oscar](https://github.com/django-oscar/django-oscar),
|
|
31
|
+
using Redsys's **redirection** integration method (the customer's browser
|
|
32
|
+
is sent to a Redsys-hosted payment page, per Redsys's own EMV 3DS/SCA
|
|
33
|
+
support for that flow).
|
|
34
|
+
|
|
35
|
+
Supports Django 5.2 (LTS) and django-oscar 4.2 only — no support for
|
|
36
|
+
older/unmaintained versions.
|
|
37
|
+
|
|
38
|
+
## What this package does, and doesn't, do
|
|
39
|
+
|
|
40
|
+
It builds and signs the `Ds_MerchantParameters`/`Ds_Signature` payload
|
|
41
|
+
for a redirect (`oscar_redsys.facade.RedsysFacade`), provides the view
|
|
42
|
+
that receives and verifies Redsys's async notification
|
|
43
|
+
(`oscar_redsys.views.NotificationView`), and fires a
|
|
44
|
+
`payment_confirmed`/`payment_declined` Django signal
|
|
45
|
+
(`oscar_redsys.signals`) for the host project to hook into its own order
|
|
46
|
+
flow.
|
|
47
|
+
|
|
48
|
+
It does **not** decide what "payment confirmed" means for your Order
|
|
49
|
+
model, doesn't place orders, and doesn't ever trust the browser-redirect
|
|
50
|
+
(`Ds_Merchant_UrlOK`/`Ds_Merchant_UrlKO`) path to confirm a payment — only
|
|
51
|
+
the signed, server-to-server notification does that. See
|
|
52
|
+
`oscar_redsys/views.py`'s module docstring for why.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
uv add django-oscar-redsys
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Add to `INSTALLED_APPS`:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
INSTALLED_APPS = [
|
|
64
|
+
...,
|
|
65
|
+
"oscar_redsys",
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Required settings (from Redsys's merchant portal — "Configuración del
|
|
70
|
+
Comercio" → "Ver clave de firma"):
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
REDSYS_MERCHANT_CODE = "999008881"
|
|
74
|
+
REDSYS_TERMINAL = "1"
|
|
75
|
+
REDSYS_SECRET_KEY = "..." # keep this out of version control
|
|
76
|
+
REDSYS_SANDBOX = True # False in production
|
|
77
|
+
REDSYS_MERCHANT_URL = "https://example.com/redsys/notify/"
|
|
78
|
+
REDSYS_URL_OK = "https://example.com/redsys/return/?ok=1"
|
|
79
|
+
REDSYS_URL_KO = "https://example.com/redsys/return/?ok=0"
|
|
80
|
+
# REDSYS_CURRENCY defaults to "978" (EUR, ISO 4217)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Wire the notification/return endpoints:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
urlpatterns = [
|
|
87
|
+
path("redsys/", include("oscar_redsys.urls")),
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run this package's migrations (`oscar_redsys.RedsysNotification`, the
|
|
92
|
+
notification audit/idempotency log, and `oscar_redsys.RedsysOperation`,
|
|
93
|
+
the refund/cancellation audit log) as part of the host project's own
|
|
94
|
+
`migrate`.
|
|
95
|
+
|
|
96
|
+
## Building the redirect
|
|
97
|
+
|
|
98
|
+
Subclass `oscar_redsys.views.PaymentRedirectView`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from decimal import Decimal
|
|
102
|
+
from oscar_redsys.views import PaymentRedirectView
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
106
|
+
def get_order_number(self) -> str:
|
|
107
|
+
return self.request.basket.order_number # whatever generates yours
|
|
108
|
+
|
|
109
|
+
def get_amount(self) -> Decimal:
|
|
110
|
+
return self.request.basket.total_incl_tax
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This renders an auto-submitting form pointed at Redsys's payment page.
|
|
114
|
+
`get_order_number()` must return Redsys's own format: at most 12
|
|
115
|
+
characters, the first 4 numeric, the rest (if any) plain ASCII
|
|
116
|
+
digits/letters — validated up front (`ValueError` if not), rather than
|
|
117
|
+
letting a malformed order fail opaquely at Redsys's end.
|
|
118
|
+
|
|
119
|
+
## EMV3DS / SCA (optional, but improves checkout friction)
|
|
120
|
+
|
|
121
|
+
PSD2 requires Strong Customer Authentication (SCA) on most card
|
|
122
|
+
payments; Redsys's EMV 3DS flow decides per transaction whether the
|
|
123
|
+
issuer can authenticate the cardholder "frictionlessly" or must
|
|
124
|
+
"challenge" them. Neither of the following is required for a payment to
|
|
125
|
+
work, but supplying them lets more transactions go frictionless:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from oscar_redsys.emv3ds import Emv3dsData, MobilePhone, ScaExemption
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def get_emv3ds(self) -> Emv3dsData:
|
|
135
|
+
return Emv3dsData(
|
|
136
|
+
ship_addr_country="724", # numeric ISO 3166-1, e.g. Spain
|
|
137
|
+
cardholder_name=self.request.user.get_full_name(),
|
|
138
|
+
email=self.request.user.email,
|
|
139
|
+
mobile_phone=MobilePhone(country_code="34", subscriber="600123456"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_sca_exemption(self) -> str | None:
|
|
143
|
+
if self.get_amount() <= Decimal("30.00"):
|
|
144
|
+
return ScaExemption.LOW_VALUE
|
|
145
|
+
return None
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`ScaExemption` only defines the three values confirmed against Redsys's
|
|
149
|
+
own PSD2/SCA documentation (`LWV`, `TRA`, `MIT`) — pass any other code
|
|
150
|
+
Redsys documents as a plain string, it isn't restricted to these.
|
|
151
|
+
|
|
152
|
+
## Consumer language (cosmetic)
|
|
153
|
+
|
|
154
|
+
`get_consumer_language()` sets what language *Redsys's own hosted pages*
|
|
155
|
+
(the payment form, the "Recibo Redsys" confirmation screen) render in —
|
|
156
|
+
it has no effect on the payment itself:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from oscar_redsys.language import ConsumerLanguage
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def get_consumer_language(self) -> str | None:
|
|
166
|
+
return ConsumerLanguage.SPANISH if self.request.LANGUAGE_CODE == "es" else ConsumerLanguage.ENGLISH
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Handling the outcome
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from django.dispatch import receiver
|
|
173
|
+
from oscar_redsys.signals import payment_confirmed, payment_declined
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@receiver(payment_confirmed)
|
|
177
|
+
def on_payment_confirmed(sender, order_number, notification, **kwargs):
|
|
178
|
+
# place the order / trigger the Dolibarr sync chain / etc.
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@receiver(payment_declined)
|
|
183
|
+
def on_payment_declined(sender, order_number, notification, **kwargs):
|
|
184
|
+
...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Both signals fire **at most once** per order number — a resent Redsys
|
|
188
|
+
notification (Redsys explicitly documents that it can resend) is a no-op
|
|
189
|
+
the second time, per `RedsysNotification`'s uniqueness on `order_number`.
|
|
190
|
+
|
|
191
|
+
## Refunds and cancellations — staff/admin only, never storefront
|
|
192
|
+
|
|
193
|
+
Unlike a payment, a refund or cancellation has no browser redirect at
|
|
194
|
+
all: it's a direct server-to-server call (`oscar_redsys.rest`, Redsys's
|
|
195
|
+
separate REST channel) referencing the original order, with no customer
|
|
196
|
+
involved. This package deliberately exposes it **only** as two Django
|
|
197
|
+
admin actions on `RedsysNotification` — "Refund selected payments" and
|
|
198
|
+
"Cancel selected payments" — never as a storefront view or URL a shopper
|
|
199
|
+
could reach.
|
|
200
|
+
|
|
201
|
+
Both actions are gated by the `oscar_redsys.can_refund_or_cancel`
|
|
202
|
+
permission on top of Django admin's own `is_staff` requirement — a
|
|
203
|
+
plain staff user can view payment records but can't trigger either
|
|
204
|
+
action without that permission explicitly granted (grant it via Django's
|
|
205
|
+
own admin, `Permission` model, or a group). Every attempt, successful or
|
|
206
|
+
not, is logged to `RedsysOperation` (who triggered it, when, the amount,
|
|
207
|
+
the outcome) — nothing here fires silently.
|
|
208
|
+
|
|
209
|
+
Refunds default to the full originally-authorized amount (recovered from
|
|
210
|
+
the stored notification's own `Ds_Amount`/`Ds_Currency`); there's no
|
|
211
|
+
partial-refund UI yet — see `oscar_redsys.rest.build_refund_request` if
|
|
212
|
+
you need a different amount from your own code.
|
|
213
|
+
|
|
214
|
+
## Signature algorithm
|
|
215
|
+
|
|
216
|
+
Two versions, both verified byte-exact against Redsys's own published
|
|
217
|
+
worked examples in `tests/test_signature.py` — see
|
|
218
|
+
`oscar_redsys/signature.py`'s module docstring for the full detail:
|
|
219
|
+
|
|
220
|
+
- **V2** (`HMAC_SHA512_V2`) for the redirection/browser flow, per
|
|
221
|
+
"TPV-Virtual Manual de Integración - Redirección" (v4.1, 24/09/2025).
|
|
222
|
+
- **V1** (`HMAC_SHA512_V1`) for the server-to-server REST channel
|
|
223
|
+
(confirm/refund/cancel), per "TPV-Virtual Manual Integración-REST"
|
|
224
|
+
(v4.0.1.1, 17/10/2025).
|
|
225
|
+
|
|
226
|
+
Both derive a per-operation key via AES-128-CBC (merchant secret key
|
|
227
|
+
forced to 16 bytes, zero IV, diversified by the order number, then
|
|
228
|
+
**base64-encoded** — that base64 string's ASCII bytes are the actual
|
|
229
|
+
HMAC key, not the raw ciphertext, a detail easy to get wrong silently
|
|
230
|
+
since a self-consistent sign/verify round trip "works" either way and
|
|
231
|
+
only breaks against Redsys itself). They differ only in the final
|
|
232
|
+
encoding: V2 uses URL-safe base64 with padding stripped, V1 uses
|
|
233
|
+
standard base64 with padding kept.
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
uv sync
|
|
239
|
+
uv run pytest
|
|
240
|
+
uv run mypy src
|
|
241
|
+
uv run black --check src tests
|
|
242
|
+
uv run isort --check src tests
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# django-oscar-redsys
|
|
2
|
+
|
|
3
|
+
Redsys (TPV Virtual) payment integration for [django-oscar](https://github.com/django-oscar/django-oscar),
|
|
4
|
+
using Redsys's **redirection** integration method (the customer's browser
|
|
5
|
+
is sent to a Redsys-hosted payment page, per Redsys's own EMV 3DS/SCA
|
|
6
|
+
support for that flow).
|
|
7
|
+
|
|
8
|
+
Supports Django 5.2 (LTS) and django-oscar 4.2 only — no support for
|
|
9
|
+
older/unmaintained versions.
|
|
10
|
+
|
|
11
|
+
## What this package does, and doesn't, do
|
|
12
|
+
|
|
13
|
+
It builds and signs the `Ds_MerchantParameters`/`Ds_Signature` payload
|
|
14
|
+
for a redirect (`oscar_redsys.facade.RedsysFacade`), provides the view
|
|
15
|
+
that receives and verifies Redsys's async notification
|
|
16
|
+
(`oscar_redsys.views.NotificationView`), and fires a
|
|
17
|
+
`payment_confirmed`/`payment_declined` Django signal
|
|
18
|
+
(`oscar_redsys.signals`) for the host project to hook into its own order
|
|
19
|
+
flow.
|
|
20
|
+
|
|
21
|
+
It does **not** decide what "payment confirmed" means for your Order
|
|
22
|
+
model, doesn't place orders, and doesn't ever trust the browser-redirect
|
|
23
|
+
(`Ds_Merchant_UrlOK`/`Ds_Merchant_UrlKO`) path to confirm a payment — only
|
|
24
|
+
the signed, server-to-server notification does that. See
|
|
25
|
+
`oscar_redsys/views.py`'s module docstring for why.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
uv add django-oscar-redsys
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add to `INSTALLED_APPS`:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
INSTALLED_APPS = [
|
|
37
|
+
...,
|
|
38
|
+
"oscar_redsys",
|
|
39
|
+
]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Required settings (from Redsys's merchant portal — "Configuración del
|
|
43
|
+
Comercio" → "Ver clave de firma"):
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
REDSYS_MERCHANT_CODE = "999008881"
|
|
47
|
+
REDSYS_TERMINAL = "1"
|
|
48
|
+
REDSYS_SECRET_KEY = "..." # keep this out of version control
|
|
49
|
+
REDSYS_SANDBOX = True # False in production
|
|
50
|
+
REDSYS_MERCHANT_URL = "https://example.com/redsys/notify/"
|
|
51
|
+
REDSYS_URL_OK = "https://example.com/redsys/return/?ok=1"
|
|
52
|
+
REDSYS_URL_KO = "https://example.com/redsys/return/?ok=0"
|
|
53
|
+
# REDSYS_CURRENCY defaults to "978" (EUR, ISO 4217)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Wire the notification/return endpoints:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
urlpatterns = [
|
|
60
|
+
path("redsys/", include("oscar_redsys.urls")),
|
|
61
|
+
]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run this package's migrations (`oscar_redsys.RedsysNotification`, the
|
|
65
|
+
notification audit/idempotency log, and `oscar_redsys.RedsysOperation`,
|
|
66
|
+
the refund/cancellation audit log) as part of the host project's own
|
|
67
|
+
`migrate`.
|
|
68
|
+
|
|
69
|
+
## Building the redirect
|
|
70
|
+
|
|
71
|
+
Subclass `oscar_redsys.views.PaymentRedirectView`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from decimal import Decimal
|
|
75
|
+
from oscar_redsys.views import PaymentRedirectView
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
79
|
+
def get_order_number(self) -> str:
|
|
80
|
+
return self.request.basket.order_number # whatever generates yours
|
|
81
|
+
|
|
82
|
+
def get_amount(self) -> Decimal:
|
|
83
|
+
return self.request.basket.total_incl_tax
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This renders an auto-submitting form pointed at Redsys's payment page.
|
|
87
|
+
`get_order_number()` must return Redsys's own format: at most 12
|
|
88
|
+
characters, the first 4 numeric, the rest (if any) plain ASCII
|
|
89
|
+
digits/letters — validated up front (`ValueError` if not), rather than
|
|
90
|
+
letting a malformed order fail opaquely at Redsys's end.
|
|
91
|
+
|
|
92
|
+
## EMV3DS / SCA (optional, but improves checkout friction)
|
|
93
|
+
|
|
94
|
+
PSD2 requires Strong Customer Authentication (SCA) on most card
|
|
95
|
+
payments; Redsys's EMV 3DS flow decides per transaction whether the
|
|
96
|
+
issuer can authenticate the cardholder "frictionlessly" or must
|
|
97
|
+
"challenge" them. Neither of the following is required for a payment to
|
|
98
|
+
work, but supplying them lets more transactions go frictionless:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from oscar_redsys.emv3ds import Emv3dsData, MobilePhone, ScaExemption
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def get_emv3ds(self) -> Emv3dsData:
|
|
108
|
+
return Emv3dsData(
|
|
109
|
+
ship_addr_country="724", # numeric ISO 3166-1, e.g. Spain
|
|
110
|
+
cardholder_name=self.request.user.get_full_name(),
|
|
111
|
+
email=self.request.user.email,
|
|
112
|
+
mobile_phone=MobilePhone(country_code="34", subscriber="600123456"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_sca_exemption(self) -> str | None:
|
|
116
|
+
if self.get_amount() <= Decimal("30.00"):
|
|
117
|
+
return ScaExemption.LOW_VALUE
|
|
118
|
+
return None
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`ScaExemption` only defines the three values confirmed against Redsys's
|
|
122
|
+
own PSD2/SCA documentation (`LWV`, `TRA`, `MIT`) — pass any other code
|
|
123
|
+
Redsys documents as a plain string, it isn't restricted to these.
|
|
124
|
+
|
|
125
|
+
## Consumer language (cosmetic)
|
|
126
|
+
|
|
127
|
+
`get_consumer_language()` sets what language *Redsys's own hosted pages*
|
|
128
|
+
(the payment form, the "Recibo Redsys" confirmation screen) render in —
|
|
129
|
+
it has no effect on the payment itself:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from oscar_redsys.language import ConsumerLanguage
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CheckoutPaymentView(PaymentRedirectView):
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def get_consumer_language(self) -> str | None:
|
|
139
|
+
return ConsumerLanguage.SPANISH if self.request.LANGUAGE_CODE == "es" else ConsumerLanguage.ENGLISH
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Handling the outcome
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from django.dispatch import receiver
|
|
146
|
+
from oscar_redsys.signals import payment_confirmed, payment_declined
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@receiver(payment_confirmed)
|
|
150
|
+
def on_payment_confirmed(sender, order_number, notification, **kwargs):
|
|
151
|
+
# place the order / trigger the Dolibarr sync chain / etc.
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@receiver(payment_declined)
|
|
156
|
+
def on_payment_declined(sender, order_number, notification, **kwargs):
|
|
157
|
+
...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Both signals fire **at most once** per order number — a resent Redsys
|
|
161
|
+
notification (Redsys explicitly documents that it can resend) is a no-op
|
|
162
|
+
the second time, per `RedsysNotification`'s uniqueness on `order_number`.
|
|
163
|
+
|
|
164
|
+
## Refunds and cancellations — staff/admin only, never storefront
|
|
165
|
+
|
|
166
|
+
Unlike a payment, a refund or cancellation has no browser redirect at
|
|
167
|
+
all: it's a direct server-to-server call (`oscar_redsys.rest`, Redsys's
|
|
168
|
+
separate REST channel) referencing the original order, with no customer
|
|
169
|
+
involved. This package deliberately exposes it **only** as two Django
|
|
170
|
+
admin actions on `RedsysNotification` — "Refund selected payments" and
|
|
171
|
+
"Cancel selected payments" — never as a storefront view or URL a shopper
|
|
172
|
+
could reach.
|
|
173
|
+
|
|
174
|
+
Both actions are gated by the `oscar_redsys.can_refund_or_cancel`
|
|
175
|
+
permission on top of Django admin's own `is_staff` requirement — a
|
|
176
|
+
plain staff user can view payment records but can't trigger either
|
|
177
|
+
action without that permission explicitly granted (grant it via Django's
|
|
178
|
+
own admin, `Permission` model, or a group). Every attempt, successful or
|
|
179
|
+
not, is logged to `RedsysOperation` (who triggered it, when, the amount,
|
|
180
|
+
the outcome) — nothing here fires silently.
|
|
181
|
+
|
|
182
|
+
Refunds default to the full originally-authorized amount (recovered from
|
|
183
|
+
the stored notification's own `Ds_Amount`/`Ds_Currency`); there's no
|
|
184
|
+
partial-refund UI yet — see `oscar_redsys.rest.build_refund_request` if
|
|
185
|
+
you need a different amount from your own code.
|
|
186
|
+
|
|
187
|
+
## Signature algorithm
|
|
188
|
+
|
|
189
|
+
Two versions, both verified byte-exact against Redsys's own published
|
|
190
|
+
worked examples in `tests/test_signature.py` — see
|
|
191
|
+
`oscar_redsys/signature.py`'s module docstring for the full detail:
|
|
192
|
+
|
|
193
|
+
- **V2** (`HMAC_SHA512_V2`) for the redirection/browser flow, per
|
|
194
|
+
"TPV-Virtual Manual de Integración - Redirección" (v4.1, 24/09/2025).
|
|
195
|
+
- **V1** (`HMAC_SHA512_V1`) for the server-to-server REST channel
|
|
196
|
+
(confirm/refund/cancel), per "TPV-Virtual Manual Integración-REST"
|
|
197
|
+
(v4.0.1.1, 17/10/2025).
|
|
198
|
+
|
|
199
|
+
Both derive a per-operation key via AES-128-CBC (merchant secret key
|
|
200
|
+
forced to 16 bytes, zero IV, diversified by the order number, then
|
|
201
|
+
**base64-encoded** — that base64 string's ASCII bytes are the actual
|
|
202
|
+
HMAC key, not the raw ciphertext, a detail easy to get wrong silently
|
|
203
|
+
since a self-consistent sign/verify round trip "works" either way and
|
|
204
|
+
only breaks against Redsys itself). They differ only in the final
|
|
205
|
+
encoding: V2 uses URL-safe base64 with padding stripped, V1 uses
|
|
206
|
+
standard base64 with padding kept.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
uv sync
|
|
212
|
+
uv run pytest
|
|
213
|
+
uv run mypy src
|
|
214
|
+
uv run black --check src tests
|
|
215
|
+
uv run isort --check src tests
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-oscar-redsys"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Redsys (TPV Virtual) redirection-method payment integration for django-oscar"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "BSD-3-Clause" }
|
|
8
|
+
authors = [{ name = "Diego Cebrian", email = "diego@cebrian-it.fr" }]
|
|
9
|
+
keywords = ["django", "django-oscar", "redsys", "payments", "ecommerce", "tpv-virtual"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Framework :: Django",
|
|
12
|
+
"Framework :: Django :: 5.2",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"License :: OSI Approved :: BSD License",
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Office/Business :: Financial",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"django>=5.2,<5.3",
|
|
23
|
+
"django-oscar>=4.2,<4.3",
|
|
24
|
+
"pycryptodome>=3.20",
|
|
25
|
+
"requests>=2.31",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/hisie/django-oscar-redsys"
|
|
30
|
+
Repository = "https://github.com/hisie/django-oscar-redsys"
|
|
31
|
+
Issues = "https://github.com/hisie/django-oscar-redsys/issues"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"black",
|
|
36
|
+
"coverage",
|
|
37
|
+
"isort",
|
|
38
|
+
"mypy",
|
|
39
|
+
"pytest",
|
|
40
|
+
"pytest-django",
|
|
41
|
+
"django-stubs",
|
|
42
|
+
"types-requests",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["hatchling"]
|
|
47
|
+
build-backend = "hatchling.build"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/oscar_redsys"]
|
|
51
|
+
|
|
52
|
+
[tool.black]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = ["py312"]
|
|
55
|
+
|
|
56
|
+
[tool.isort]
|
|
57
|
+
profile = "black"
|
|
58
|
+
line_length = 100
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.12"
|
|
62
|
+
strict = true
|
|
63
|
+
plugins = ["mypy_django_plugin.main"]
|
|
64
|
+
mypy_path = "src"
|
|
65
|
+
|
|
66
|
+
[tool.django-stubs]
|
|
67
|
+
django_settings_module = "tests.settings"
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
71
|
+
python_files = ["test_*.py"]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
pythonpath = ["."]
|
|
74
|
+
|
|
75
|
+
[tool.coverage.run]
|
|
76
|
+
source = ["src/oscar_redsys"]
|