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.
Files changed (45) hide show
  1. django_oscar_redsys-0.1.0/.gitignore +9 -0
  2. django_oscar_redsys-0.1.0/.python-version +1 -0
  3. django_oscar_redsys-0.1.0/LICENSE +29 -0
  4. django_oscar_redsys-0.1.0/PKG-INFO +247 -0
  5. django_oscar_redsys-0.1.0/README.md +220 -0
  6. django_oscar_redsys-0.1.0/pyproject.toml +76 -0
  7. django_oscar_redsys-0.1.0/src/oscar_redsys/__init__.py +3 -0
  8. django_oscar_redsys-0.1.0/src/oscar_redsys/admin.py +190 -0
  9. django_oscar_redsys-0.1.0/src/oscar_redsys/apps.py +10 -0
  10. django_oscar_redsys-0.1.0/src/oscar_redsys/conf.py +57 -0
  11. django_oscar_redsys-0.1.0/src/oscar_redsys/emv3ds.py +86 -0
  12. django_oscar_redsys-0.1.0/src/oscar_redsys/facade.py +157 -0
  13. django_oscar_redsys-0.1.0/src/oscar_redsys/language.py +32 -0
  14. django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/0001_initial.py +32 -0
  15. django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/0002_redsysoperation.py +54 -0
  16. django_oscar_redsys-0.1.0/src/oscar_redsys/migrations/__init__.py +0 -0
  17. django_oscar_redsys-0.1.0/src/oscar_redsys/models.py +69 -0
  18. django_oscar_redsys-0.1.0/src/oscar_redsys/order_number.py +24 -0
  19. django_oscar_redsys-0.1.0/src/oscar_redsys/params.py +38 -0
  20. django_oscar_redsys-0.1.0/src/oscar_redsys/response_codes.py +21 -0
  21. django_oscar_redsys-0.1.0/src/oscar_redsys/rest.py +172 -0
  22. django_oscar_redsys-0.1.0/src/oscar_redsys/signals.py +22 -0
  23. django_oscar_redsys-0.1.0/src/oscar_redsys/signature.py +112 -0
  24. django_oscar_redsys-0.1.0/src/oscar_redsys/templates/oscar_redsys/redirect.html +18 -0
  25. django_oscar_redsys-0.1.0/src/oscar_redsys/templates/oscar_redsys/return.html +18 -0
  26. django_oscar_redsys-0.1.0/src/oscar_redsys/transaction_types.py +19 -0
  27. django_oscar_redsys-0.1.0/src/oscar_redsys/urls.py +12 -0
  28. django_oscar_redsys-0.1.0/src/oscar_redsys/views.py +173 -0
  29. django_oscar_redsys-0.1.0/tests/__init__.py +0 -0
  30. django_oscar_redsys-0.1.0/tests/conftest.py +24 -0
  31. django_oscar_redsys-0.1.0/tests/settings.py +61 -0
  32. django_oscar_redsys-0.1.0/tests/test_admin.py +220 -0
  33. django_oscar_redsys-0.1.0/tests/test_conf.py +28 -0
  34. django_oscar_redsys-0.1.0/tests/test_emv3ds.py +53 -0
  35. django_oscar_redsys-0.1.0/tests/test_facade.py +241 -0
  36. django_oscar_redsys-0.1.0/tests/test_language.py +26 -0
  37. django_oscar_redsys-0.1.0/tests/test_models.py +30 -0
  38. django_oscar_redsys-0.1.0/tests/test_order_number.py +36 -0
  39. django_oscar_redsys-0.1.0/tests/test_params.py +66 -0
  40. django_oscar_redsys-0.1.0/tests/test_response_codes.py +21 -0
  41. django_oscar_redsys-0.1.0/tests/test_rest.py +122 -0
  42. django_oscar_redsys-0.1.0/tests/test_signature.py +153 -0
  43. django_oscar_redsys-0.1.0/tests/test_views.py +179 -0
  44. django_oscar_redsys-0.1.0/tests/urls.py +7 -0
  45. django_oscar_redsys-0.1.0/uv.lock +1106 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .coverage
7
+ htmlcov/
8
+ dist/
9
+ *.egg-info/
@@ -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"]
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.1.0"