Skip to content

Commit 6082843

Browse files
Hugo Osvaldo BarreraWhyNotHugo
authored andcommitted
Use a JSONField for BasePayment.extra_data
1 parent 0874543 commit 6082843

File tree

17 files changed

+95
-114
lines changed

17 files changed

+95
-114
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ v3.0.0
88
------
99
- **BREAKING**: Dropped support for Django 2.2, 3.0, 3.1 and 4.0.
1010
Supported versions of Django are 3.2 (LTS), 4.1 and 4.2.
11+
- **BREAKING** ``BasePayment.extra_data`` is now a JSONField and django will
12+
handle the serialisation. Due to this, usage of the ``BasePayment.attrs``
13+
proxy has been deprecated. A migration needs to be generated to update this
14+
column in place. Application code needs to be updated from
15+
``payment.extra_data.field`` to ``payment.extra_data["field"]``.
1116
- Stripe backends now sends order_id in the metadata parameter.
1217
- A new ``StripeProviderV3`` has been added using the latest Stripe API.
1318
- Added support for Python 3.11, Django 4.1 and Django 4.2.

docs/backends.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ about the payment or the order, such as an order number, additional customer
108108
information, or a special comment or request from the customer. This can be
109109
accomplished by passing your data to the :class:`Payment` instance::
110110

111-
>>> payment.attrs.merchant_defined_data = {'01': 'foo', '02': 'bar'}
111+
>>> payment.extra_data["merchant_defined_data"] = {'01': 'foo', '02': 'bar'}
112112

113113
Fingerprinting::
114114

payments/cybersource/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,11 @@ def charge(self, payment, data):
170170
else:
171171
params = self._prepare_preauth(payment, data)
172172
response = self._make_request(payment, params)
173-
payment.attrs.capture = self._capture
173+
payment.extra_data["capture"] = self._capture
174174
payment.transaction_id = response.requestID
175175
if response.reasonCode == AUTHENTICATE_REQUIRED:
176176
xid = response.payerAuthEnrollReply.xid
177-
payment.attrs.xid = xid
177+
payment.extra_data["xid"] = xid
178178
payment.change_status(
179179
PaymentStatus.WAITING, message=_("3-D Secure verification in progress")
180180
)
@@ -276,8 +276,8 @@ def _get_params_for_new_payment(self, payment):
276276
"merchantReferenceCode": payment.id,
277277
}
278278
try:
279-
fingerprint_id = payment.attrs.fingerprint_session_id
280-
except AttributeError:
279+
fingerprint_id = payment.extra_data["fingerprint_session_id"]
280+
except KeyError:
281281
pass
282282
else:
283283
params["deviceFingerprintID"] = fingerprint_id
@@ -288,7 +288,7 @@ def _get_params_for_new_payment(self, payment):
288288

289289
def _make_request(self, payment, params):
290290
response = self.client.service.runTransaction(**params)
291-
payment.attrs.last_response = self._serialize_response(response)
291+
payment.extra_data["last_response"] = self._serialize_response(response)
292292
return response
293293

294294
def _prepare_payer_auth_validation_check(self, payment, card_data, pa_response):
@@ -297,7 +297,7 @@ def _prepare_payer_auth_validation_check(self, payment, card_data, pa_response):
297297
check_service.signedPARes = pa_response
298298
params = self._get_params_for_new_payment(payment)
299299
params["payerAuthValidateService"] = check_service
300-
if payment.attrs.capture:
300+
if payment.extra_data["capture"]:
301301
service = self.client.factory.create("data:CCCreditService")
302302
service._run = "true"
303303
params["ccCreditService"] = service
@@ -440,8 +440,8 @@ def _prepare_items(self, payment):
440440

441441
def _prepare_merchant_defined_data(self, payment):
442442
try:
443-
merchant_defined_data = payment.attrs.merchant_defined_data
444-
except AttributeError:
443+
merchant_defined_data = payment.extra_data["merchant_defined_data"]
444+
except KeyError:
445445
return None
446446
else:
447447
data = self.client.factory.create("data:MerchantDefinedData")
@@ -471,7 +471,7 @@ def _serialize_response(self, response):
471471

472472
def process_data(self, payment, request):
473473
xid = request.POST.get("MD")
474-
if xid != payment.attrs.xid:
474+
if xid != payment.extra_data["xid"]:
475475
return redirect(payment.get_failure_url())
476476
if payment.status in [PaymentStatus.CONFIRMED, PaymentStatus.PREAUTH]:
477477
return redirect(payment.get_success_url())

payments/cybersource/forms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self, *args, **kwargs):
4141
super().__init__(*args, **kwargs)
4242
if self.provider.org_id:
4343
try:
44-
fingerprint_id = self.payment.attrs.fingerprint_session_id
44+
fingerprint_id = self.payment.extra_data["fingerprint_session_id"]
4545
except KeyError:
4646
fingerprint_id = str(uuid4())
4747
self.fields["fingerprint"] = FingerprintInput(
@@ -57,7 +57,7 @@ def clean(self):
5757
if not self.errors:
5858
if self.provider.org_id:
5959
fingerprint = cleaned_data["fingerprint"]
60-
self.payment.attrs.fingerprint_session_id = fingerprint
60+
self.payment.extra_data["fingerprint_session_id"] = fingerprint
6161
if not self.payment.transaction_id:
6262
try:
6363
self.provider.charge(self.payment, cleaned_data)

payments/cybersource/test_cybersource.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ class Payment(Mock):
4141
transaction_id = None
4242
captured_amount = 0
4343
message = ""
44-
45-
class attrs:
46-
fingerprint_session_id = "fake"
47-
merchant_defined_data: dict[str, str] = {}
44+
extra_data = {
45+
"fingerprint_session_id": "fake",
46+
"merchant_defined_data": {},
47+
}
4848

4949
def get_process_url(self):
5050
return "http://example.com"
@@ -153,7 +153,7 @@ def test_provider_redirects_on_success_captured_payment(
153153
):
154154
transaction_id = 1234
155155
xid = "abc"
156-
self.payment.attrs.xid = xid
156+
self.payment.extra_data["xid"] = xid
157157

158158
response = MagicMock()
159159
response.requestID = transaction_id
@@ -188,7 +188,7 @@ def test_provider_redirects_on_success_preauth_payment(
188188
)
189189
transaction_id = 1234
190190
xid = "abc"
191-
self.payment.attrs.xid = xid
191+
self.payment.extra_data["xid"] = xid
192192

193193
response = MagicMock()
194194
response.requestID = transaction_id
@@ -218,7 +218,7 @@ def test_provider_redirects_on_success_preauth_payment(
218218
def test_provider_redirects_on_failure(self, mocked_request, mocked_redirect):
219219
transaction_id = 1234
220220
xid = "abc"
221-
self.payment.attrs.xid = xid
221+
self.payment.extra_data["xid"] = xid
222222

223223
response = MagicMock()
224224
response.requestID = transaction_id

payments/mercadopago/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def create_preference(self, payment: BasePayment):
7474
if payment.transaction_id:
7575
raise ValueError("This payment already has a preference.")
7676

77-
payment.attrs.external_reference = uuid4().hex
77+
payment.extra_data["external_reference"] = uuid4().hex
7878

7979
payload = {
8080
"auto_return": "all",
@@ -89,7 +89,7 @@ def create_preference(self, payment: BasePayment):
8989
}
9090
for item in payment.get_purchased_items()
9191
],
92-
"external_reference": payment.attrs.external_reference,
92+
"external_reference": payment.extra_data["external_reference"],
9393
"back_urls": {
9494
"success": self.get_return_url(payment),
9595
"pending": self.get_return_url(payment),
@@ -218,7 +218,7 @@ def poll_for_updates(self, payment: BasePayment):
218218
"""
219219
data = self.client.payment().search(
220220
{
221-
"external_reference": payment.attrs.external_reference,
221+
"external_reference": payment.extra_data["external_reference"],
222222
}
223223
)
224224

payments/mercadopago/test_mercadopago.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Payment(Mock):
2929
captured_amount = 0
3030
transaction_id: str | None = None
3131
billing_email = "[email protected]"
32+
extra_data: dict = {}
3233

3334
def change_status(self, status, message=""):
3435
self.status = status

payments/models.py

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
import json
3+
import warnings
44
from typing import Iterable
55
from uuid import uuid4
66

@@ -15,30 +15,6 @@
1515
from .core import provider_factory
1616

1717

18-
class PaymentAttributeProxy:
19-
def __init__(self, payment):
20-
self._payment = payment
21-
super().__init__()
22-
23-
def __getattr__(self, item):
24-
data = json.loads(self._payment.extra_data or "{}")
25-
try:
26-
return data[item]
27-
except KeyError as e:
28-
raise AttributeError(*e.args) from e
29-
30-
def __setattr__(self, key, value):
31-
if key == "_payment":
32-
return super().__setattr__(key, value)
33-
try:
34-
data = json.loads(self._payment.extra_data)
35-
except ValueError:
36-
data = {}
37-
data[key] = value
38-
self._payment.extra_data = json.dumps(data)
39-
return None
40-
41-
4218
class BasePayment(models.Model):
4319
"""
4420
Represents a single transaction. Each instance has one or more PaymentItem.
@@ -80,7 +56,7 @@ class BasePayment(models.Model):
8056
billing_email = models.EmailField(blank=True)
8157
billing_phone = PhoneNumberField(blank=True)
8258
customer_ip_address = models.GenericIPAddressField(blank=True, null=True)
83-
extra_data = models.TextField(blank=True, default="")
59+
extra_data = models.JSONField(blank=True, default=dict)
8460
message = models.TextField(blank=True, default="")
8561
token = models.CharField(max_length=36, blank=True, default="")
8662
captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default="0.0")
@@ -226,14 +202,18 @@ def refund(self, amount=None):
226202

227203
@property
228204
def attrs(self):
229-
"""A JSON-serialised wrapper around `extra_data`.
230-
231-
This property exposes a a dict or list which is serialised into the `extra_data`
232-
text field. Usage of this wrapper is preferred over accessing the underlying
233-
field directly.
234-
235-
You may think of this as a `JSONField` which is saved to the `extra_data`
236-
column.
237-
"""
238-
# TODO: Deprecate in favour of JSONField when we drop support for django 2.2.
239-
return PaymentAttributeProxy(self)
205+
warnings.warn(
206+
"Using BasePayment.attrs is deprecated. Use BasePayment.extra_data instead",
207+
DeprecationWarning,
208+
stacklevel=2,
209+
)
210+
return self.extra_data
211+
212+
@attrs.setter
213+
def attrs(self, value):
214+
warnings.warn(
215+
"Using BasePayment.attrs is deprecated. Use BasePayment.extra_data instead",
216+
DeprecationWarning,
217+
stacklevel=2,
218+
)
219+
self.extra_data = value

payments/paypal/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,34 +82,34 @@ def __init__(
8282
super().__init__(capture=capture)
8383

8484
def set_response_data(self, payment, response, is_auth=False):
85-
extra_data = json.loads(payment.extra_data or "{}")
85+
extra_data = payment.extra_data or {}
8686
if is_auth:
8787
extra_data["auth_response"] = response
8888
else:
8989
extra_data["response"] = response
9090
if "links" in response:
9191
extra_data["links"] = {link["rel"]: link for link in response["links"]}
92-
payment.extra_data = json.dumps(extra_data)
92+
payment.extra_data = extra_data
9393
payment.save()
9494

9595
def set_response_links(self, payment, response):
9696
transaction = response["transactions"][0]
9797
related_resources = transaction["related_resources"][0]
9898
resource_key = "sale" if self._capture else "authorization"
9999
links = related_resources[resource_key]["links"]
100-
extra_data = json.loads(payment.extra_data or "{}")
100+
extra_data = payment.extra_data or {}
101101
extra_data["links"] = {link["rel"]: link for link in links}
102-
payment.extra_data = json.dumps(extra_data)
102+
payment.extra_data = extra_data
103103
payment.save()
104104

105105
def set_error_data(self, payment, error):
106-
extra_data = json.loads(payment.extra_data or "{}")
106+
extra_data = payment.extra_data or {}
107107
extra_data["error"] = error
108-
payment.extra_data = json.dumps(extra_data)
108+
payment.extra_data = extra_data
109109
payment.save()
110110

111111
def _get_links(self, payment):
112-
extra_data = json.loads(payment.extra_data or "{}")
112+
extra_data = payment.extra_data or {}
113113
return extra_data.get("links", {})
114114

115115
@authorize
@@ -144,7 +144,7 @@ def post(self, payment, *args, **kwargs):
144144
return data
145145

146146
def get_last_response(self, payment, is_auth=False):
147-
extra_data = json.loads(payment.extra_data or "{}")
147+
extra_data = payment.extra_data or {}
148148
if is_auth:
149149
return extra_data.get("auth_response", {})
150150
return extra_data.get("response", {})
@@ -249,7 +249,7 @@ def process_data(self, payment, request):
249249
except PaymentError:
250250
return redirect(failure_url)
251251
self.set_response_links(payment, executed_payment)
252-
payment.attrs.payer_info = executed_payment["payer"]["payer_info"]
252+
payment.extra_data["payer_info"] = executed_payment["payer"]["payer_info"]
253253
if self._capture:
254254
payment.captured_amount = payment.total
255255
payment.change_status(PaymentStatus.CONFIRMED)

payments/paypal/test_paypal.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import json
43
from datetime import date
54
from decimal import Decimal
65
from unittest import TestCase
@@ -46,16 +45,14 @@ class Payment(Mock):
4645
variant = VARIANT
4746
transaction_id = None
4847
message = ""
49-
extra_data = json.dumps(
50-
{
51-
"links": {
52-
"approval_url": None,
53-
"capture": {"href": "http://capture.com"},
54-
"refund": {"href": "http://refund.com"},
55-
"execute": {"href": "http://execute.com"},
56-
}
48+
extra_data = {
49+
"links": {
50+
"approval_url": None,
51+
"capture": {"href": "http://capture.com"},
52+
"refund": {"href": "http://refund.com"},
53+
"execute": {"href": "http://execute.com"},
5754
}
58-
)
55+
}
5956

6057
def change_status(self, status, message=""):
6158
self.status = status
@@ -225,23 +222,22 @@ def test_provider_renews_access_token(self, mocked_post):
225222
mocked_post.side_effect = [HTTPError(response=response401), response, response]
226223

227224
self.payment.created = timezone.now()
228-
self.payment.extra_data = json.dumps(
229-
{
230-
"auth_response": {
231-
"access_token": "expired_token",
232-
"token_type": "token type",
233-
"expires_in": 99999,
234-
}
225+
self.payment.extra_data = {
226+
"auth_response": {
227+
"access_token": "expired_token",
228+
"token_type": "token type",
229+
"expires_in": 99999,
235230
}
236-
)
231+
}
232+
237233
self.provider.create_payment(self.payment)
238-
payment_response = json.loads(self.payment.extra_data)["auth_response"]
234+
payment_response = self.payment.extra_data["auth_response"]
239235
self.assertEqual(payment_response["access_token"], new_token)
240236

241237

242238
class TestPaypalCardProvider(TestCase):
243239
def setUp(self):
244-
self.payment = Payment(extra_data="")
240+
self.payment = Payment(extra_data={})
245241
self.provider = PaypalCardProvider(secret=SECRET, client_id=CLIENT_ID)
246242

247243
def test_provider_raises_redirect_needed_on_success_captured_payment(self):

0 commit comments

Comments
 (0)