Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JSONField for BasePayment.attrs #306

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ v3.0.0
------
- **BREAKING**: Dropped support for Django 2.2, 3.0, 3.1 and 4.0.
Supported versions of Django are 3.2 (LTS), 4.1 and 4.2.
- **BREAKING** ``BasePayment.extra_data`` is now a JSONField and django will
handle the serialisation. Due to this, usage of the ``BasePayment.attrs``
proxy has been deprecated. A migration needs to be generated to update this
column in place. Application code needs to be updated from
``payment.extra_data.field`` to ``payment.extra_data["field"]``.
- Stripe backends now sends order_id in the metadata parameter.
- A new ``StripeProviderV3`` has been added using the latest Stripe API.
- Added support for Python 3.11, Django 4.1 and Django 4.2.
Expand Down
2 changes: 1 addition & 1 deletion docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ about the payment or the order, such as an order number, additional customer
information, or a special comment or request from the customer. This can be
accomplished by passing your data to the :class:`Payment` instance::

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

Fingerprinting::

Expand Down
18 changes: 9 additions & 9 deletions payments/cybersource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@
else:
params = self._prepare_preauth(payment, data)
response = self._make_request(payment, params)
payment.attrs.capture = self._capture
payment.extra_data["capture"] = self._capture
payment.transaction_id = response.requestID
if response.reasonCode == AUTHENTICATE_REQUIRED:
xid = response.payerAuthEnrollReply.xid
payment.attrs.xid = xid
payment.extra_data["xid"] = xid
payment.change_status(
PaymentStatus.WAITING, message=_("3-D Secure verification in progress")
)
Expand Down Expand Up @@ -276,8 +276,8 @@
"merchantReferenceCode": payment.id,
}
try:
fingerprint_id = payment.attrs.fingerprint_session_id
except AttributeError:
fingerprint_id = payment.extra_data["fingerprint_session_id"]
except KeyError:

Check warning on line 280 in payments/cybersource/__init__.py

View check run for this annotation

Codecov / codecov/patch

payments/cybersource/__init__.py#L280

Added line #L280 was not covered by tests
pass
else:
params["deviceFingerprintID"] = fingerprint_id
Expand All @@ -288,7 +288,7 @@

def _make_request(self, payment, params):
response = self.client.service.runTransaction(**params)
payment.attrs.last_response = self._serialize_response(response)
payment.extra_data["last_response"] = self._serialize_response(response)

Check warning on line 291 in payments/cybersource/__init__.py

View check run for this annotation

Codecov / codecov/patch

payments/cybersource/__init__.py#L291

Added line #L291 was not covered by tests
return response

def _prepare_payer_auth_validation_check(self, payment, card_data, pa_response):
Expand All @@ -297,7 +297,7 @@
check_service.signedPARes = pa_response
params = self._get_params_for_new_payment(payment)
params["payerAuthValidateService"] = check_service
if payment.attrs.capture:
if payment.extra_data["capture"]:
service = self.client.factory.create("data:CCCreditService")
service._run = "true"
params["ccCreditService"] = service
Expand Down Expand Up @@ -440,8 +440,8 @@

def _prepare_merchant_defined_data(self, payment):
try:
merchant_defined_data = payment.attrs.merchant_defined_data
except AttributeError:
merchant_defined_data = payment.extra_data["merchant_defined_data"]
except KeyError:

Check warning on line 444 in payments/cybersource/__init__.py

View check run for this annotation

Codecov / codecov/patch

payments/cybersource/__init__.py#L444

Added line #L444 was not covered by tests
return None
else:
data = self.client.factory.create("data:MerchantDefinedData")
Expand Down Expand Up @@ -471,7 +471,7 @@

def process_data(self, payment, request):
xid = request.POST.get("MD")
if xid != payment.attrs.xid:
if xid != payment.extra_data["xid"]:
return redirect(payment.get_failure_url())
if payment.status in [PaymentStatus.CONFIRMED, PaymentStatus.PREAUTH]:
return redirect(payment.get_success_url())
Expand Down
4 changes: 2 additions & 2 deletions payments/cybersource/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.provider.org_id:
try:
fingerprint_id = self.payment.attrs.fingerprint_session_id
fingerprint_id = self.payment.extra_data["fingerprint_session_id"]
except KeyError:
fingerprint_id = str(uuid4())
self.fields["fingerprint"] = FingerprintInput(
Expand All @@ -57,7 +57,7 @@ def clean(self):
if not self.errors:
if self.provider.org_id:
fingerprint = cleaned_data["fingerprint"]
self.payment.attrs.fingerprint_session_id = fingerprint
self.payment.extra_data["fingerprint_session_id"] = fingerprint
if not self.payment.transaction_id:
try:
self.provider.charge(self.payment, cleaned_data)
Expand Down
14 changes: 7 additions & 7 deletions payments/cybersource/test_cybersource.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ class Payment(Mock):
transaction_id = None
captured_amount = 0
message = ""

class attrs:
fingerprint_session_id = "fake"
merchant_defined_data: dict[str, str] = {}
extra_data = {
"fingerprint_session_id": "fake",
"merchant_defined_data": {},
}

def get_process_url(self):
return "http://example.com"
Expand Down Expand Up @@ -153,7 +153,7 @@ def test_provider_redirects_on_success_captured_payment(
):
transaction_id = 1234
xid = "abc"
self.payment.attrs.xid = xid
self.payment.extra_data["xid"] = xid

response = MagicMock()
response.requestID = transaction_id
Expand Down Expand Up @@ -188,7 +188,7 @@ def test_provider_redirects_on_success_preauth_payment(
)
transaction_id = 1234
xid = "abc"
self.payment.attrs.xid = xid
self.payment.extra_data["xid"] = xid

response = MagicMock()
response.requestID = transaction_id
Expand Down Expand Up @@ -218,7 +218,7 @@ def test_provider_redirects_on_success_preauth_payment(
def test_provider_redirects_on_failure(self, mocked_request, mocked_redirect):
transaction_id = 1234
xid = "abc"
self.payment.attrs.xid = xid
self.payment.extra_data["xid"] = xid

response = MagicMock()
response.requestID = transaction_id
Expand Down
6 changes: 3 additions & 3 deletions payments/mercadopago/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def create_preference(self, payment: BasePayment):
if payment.transaction_id:
raise ValueError("This payment already has a preference.")

payment.attrs.external_reference = uuid4().hex
payment.extra_data["external_reference"] = uuid4().hex

payload = {
"auto_return": "all",
Expand All @@ -89,7 +89,7 @@ def create_preference(self, payment: BasePayment):
}
for item in payment.get_purchased_items()
],
"external_reference": payment.attrs.external_reference,
"external_reference": payment.extra_data["external_reference"],
"back_urls": {
"success": self.get_return_url(payment),
"pending": self.get_return_url(payment),
Expand Down Expand Up @@ -218,7 +218,7 @@ def poll_for_updates(self, payment: BasePayment):
"""
data = self.client.payment().search(
{
"external_reference": payment.attrs.external_reference,
"external_reference": payment.extra_data["external_reference"],
}
)

Expand Down
1 change: 1 addition & 0 deletions payments/mercadopago/test_mercadopago.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Payment(Mock):
captured_amount = 0
transaction_id: str | None = None
billing_email = "[email protected]"
extra_data: dict = {}

def change_status(self, status, message=""):
self.status = status
Expand Down
54 changes: 17 additions & 37 deletions payments/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

import json
import warnings
from typing import Iterable
from uuid import uuid4

Expand All @@ -15,30 +15,6 @@
from .core import provider_factory


class PaymentAttributeProxy:
def __init__(self, payment):
self._payment = payment
super().__init__()

def __getattr__(self, item):
data = json.loads(self._payment.extra_data or "{}")
try:
return data[item]
except KeyError as e:
raise AttributeError(*e.args) from e

def __setattr__(self, key, value):
if key == "_payment":
return super().__setattr__(key, value)
try:
data = json.loads(self._payment.extra_data)
except ValueError:
data = {}
data[key] = value
self._payment.extra_data = json.dumps(data)
return None


class BasePayment(models.Model):
"""
Represents a single transaction. Each instance has one or more PaymentItem.
Expand Down Expand Up @@ -80,7 +56,7 @@
billing_email = models.EmailField(blank=True)
billing_phone = PhoneNumberField(blank=True)
customer_ip_address = models.GenericIPAddressField(blank=True, null=True)
extra_data = models.TextField(blank=True, default="")
extra_data = models.JSONField(blank=True, default=dict)
message = models.TextField(blank=True, default="")
token = models.CharField(max_length=36, blank=True, default="")
captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default="0.0")
Expand Down Expand Up @@ -226,14 +202,18 @@

@property
def attrs(self):
"""A JSON-serialised wrapper around `extra_data`.

This property exposes a a dict or list which is serialised into the `extra_data`
text field. Usage of this wrapper is preferred over accessing the underlying
field directly.

You may think of this as a `JSONField` which is saved to the `extra_data`
column.
"""
# TODO: Deprecate in favour of JSONField when we drop support for django 2.2.
return PaymentAttributeProxy(self)
warnings.warn(

Check warning on line 205 in payments/models.py

View check run for this annotation

Codecov / codecov/patch

payments/models.py#L205

Added line #L205 was not covered by tests
"Using BasePayment.attrs is deprecated. Use BasePayment.extra_data instead",
DeprecationWarning,
stacklevel=2,
)
return self.extra_data

Check warning on line 210 in payments/models.py

View check run for this annotation

Codecov / codecov/patch

payments/models.py#L210

Added line #L210 was not covered by tests

@attrs.setter
def attrs(self, value):
warnings.warn(

Check warning on line 214 in payments/models.py

View check run for this annotation

Codecov / codecov/patch

payments/models.py#L214

Added line #L214 was not covered by tests
"Using BasePayment.attrs is deprecated. Use BasePayment.extra_data instead",
DeprecationWarning,
stacklevel=2,
)
self.extra_data = value

Check warning on line 219 in payments/models.py

View check run for this annotation

Codecov / codecov/patch

payments/models.py#L219

Added line #L219 was not covered by tests
18 changes: 9 additions & 9 deletions payments/paypal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,34 +82,34 @@
super().__init__(capture=capture)

def set_response_data(self, payment, response, is_auth=False):
extra_data = json.loads(payment.extra_data or "{}")
extra_data = payment.extra_data or {}
if is_auth:
extra_data["auth_response"] = response
else:
extra_data["response"] = response
if "links" in response:
extra_data["links"] = {link["rel"]: link for link in response["links"]}
payment.extra_data = json.dumps(extra_data)
payment.extra_data = extra_data
payment.save()

def set_response_links(self, payment, response):
transaction = response["transactions"][0]
related_resources = transaction["related_resources"][0]
resource_key = "sale" if self._capture else "authorization"
links = related_resources[resource_key]["links"]
extra_data = json.loads(payment.extra_data or "{}")
extra_data = payment.extra_data or {}
extra_data["links"] = {link["rel"]: link for link in links}
payment.extra_data = json.dumps(extra_data)
payment.extra_data = extra_data
payment.save()

def set_error_data(self, payment, error):
extra_data = json.loads(payment.extra_data or "{}")
extra_data = payment.extra_data or {}
extra_data["error"] = error
payment.extra_data = json.dumps(extra_data)
payment.extra_data = extra_data
payment.save()

def _get_links(self, payment):
extra_data = json.loads(payment.extra_data or "{}")
extra_data = payment.extra_data or {}
return extra_data.get("links", {})

@authorize
Expand Down Expand Up @@ -144,7 +144,7 @@
return data

def get_last_response(self, payment, is_auth=False):
extra_data = json.loads(payment.extra_data or "{}")
extra_data = payment.extra_data or {}
if is_auth:
return extra_data.get("auth_response", {})
return extra_data.get("response", {})
Expand Down Expand Up @@ -249,7 +249,7 @@
except PaymentError:
return redirect(failure_url)
self.set_response_links(payment, executed_payment)
payment.attrs.payer_info = executed_payment["payer"]["payer_info"]
payment.extra_data["payer_info"] = executed_payment["payer"]["payer_info"]

Check warning on line 252 in payments/paypal/__init__.py

View check run for this annotation

Codecov / codecov/patch

payments/paypal/__init__.py#L252

Added line #L252 was not covered by tests
if self._capture:
payment.captured_amount = payment.total
payment.change_status(PaymentStatus.CONFIRMED)
Expand Down
36 changes: 16 additions & 20 deletions payments/paypal/test_paypal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import json
from datetime import date
from decimal import Decimal
from unittest import TestCase
Expand Down Expand Up @@ -46,16 +45,14 @@ class Payment(Mock):
variant = VARIANT
transaction_id = None
message = ""
extra_data = json.dumps(
{
"links": {
"approval_url": None,
"capture": {"href": "http://capture.com"},
"refund": {"href": "http://refund.com"},
"execute": {"href": "http://execute.com"},
}
extra_data = {
"links": {
"approval_url": None,
"capture": {"href": "http://capture.com"},
"refund": {"href": "http://refund.com"},
"execute": {"href": "http://execute.com"},
}
)
}

def change_status(self, status, message=""):
self.status = status
Expand Down Expand Up @@ -225,23 +222,22 @@ def test_provider_renews_access_token(self, mocked_post):
mocked_post.side_effect = [HTTPError(response=response401), response, response]

self.payment.created = timezone.now()
self.payment.extra_data = json.dumps(
{
"auth_response": {
"access_token": "expired_token",
"token_type": "token type",
"expires_in": 99999,
}
self.payment.extra_data = {
"auth_response": {
"access_token": "expired_token",
"token_type": "token type",
"expires_in": 99999,
}
)
}

self.provider.create_payment(self.payment)
payment_response = json.loads(self.payment.extra_data)["auth_response"]
payment_response = self.payment.extra_data["auth_response"]
self.assertEqual(payment_response["access_token"], new_token)


class TestPaypalCardProvider(TestCase):
def setUp(self):
self.payment = Payment(extra_data="")
self.payment = Payment(extra_data={})
self.provider = PaypalCardProvider(secret=SECRET, client_id=CLIENT_ID)

def test_provider_raises_redirect_needed_on_success_captured_payment(self):
Expand Down
4 changes: 2 additions & 2 deletions payments/sofort/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def process_data(self, payment, request):
else:
payment.captured_amount = payment.total
payment.change_status(PaymentStatus.CONFIRMED)
payment.extra_data = json.dumps(doc)
payment.extra_data = doc
sender_data = doc["transactions"]["transaction_details"]["sender"]
holder_data = sender_data["holder"]
first_name, last_name = holder_data.rsplit(" ", 1)
Expand All @@ -114,7 +114,7 @@ def process_data(self, payment, request):
def refund(self, payment, amount=None):
if amount is None:
amount = payment.captured_amount
doc = json.loads(payment.extra_data)
doc = payment.extra_data
sender_data = doc["transactions"]["transaction_details"]["sender"]
refund_request = render_to_string(
"payments/sofort/refund_transaction.xml",
Expand Down
Loading
Loading