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

PayPal: Save captured_amount when processing data #412

Merged
merged 1 commit into from
Jun 11, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ v3.0.0
- Added support for Python 3.11, Django 4.1 and Django 4.2.
- Stripe backends now supports webhooks
- New :ref:`webhook settings <webhooks>`
- Fixed PayPal backends not saving captured_amount when processing data.

v2.0.0
------
Expand Down
3 changes: 3 additions & 0 deletions payments/paypal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ def process_data(self, payment, request):
payment.attrs.payer_info = executed_payment["payer"]["payer_info"]
if self._capture:
payment.captured_amount = payment.total
type(payment).objects.filter(pk=payment.pk).update(
captured_amount=payment.captured_amount
)
payment.change_status(PaymentStatus.CONFIRMED)
else:
payment.change_status(PaymentStatus.PREAUTH)
Expand Down
115 changes: 114 additions & 1 deletion payments/paypal/test_paypal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
from copy import deepcopy
from datetime import date
from decimal import Decimal
from unittest import TestCase
Expand Down Expand Up @@ -33,7 +34,58 @@
}


class PaymentQuerySet(Mock):
__payments = {}

def create(self, **kwargs):
if kwargs:
raise NotImplementedError(f"arguments not supported yet: {kwargs}")
id_ = max(self.__payments) + 1 if self.__payments else 1
self.__payments[id_] = {}
payment = Payment()
payment.id = id_
payment.save()
return payment

def get(self, *args, **kwargs):
if args or kwargs:
return self.filter(*args, **kwargs).get()
payment = Payment()
(payment_fields,) = self.__payments.values()
for payment_field_name, payment_field_value in payment_fields.items():
setattr(payment, payment_field_name, deepcopy(payment_field_value))
return payment

def filter(self, *args, pk=None, **kwargs):
if args or kwargs:
raise NotImplementedError(f"arguments not supported yet: {args}, {kwargs}")
if pk is not None:
return PaymentQuerySet(
{pk_: payment for pk_, payment in self.__payments.items() if pk_ == pk}
)
return self

def update(self, **kwargs):
for payment in self.__payments.values():
for field_name, field_value in kwargs.items():
if not any(
field.name == field_name
for field in Payment._meta.get_fields(
include_parents=True, include_hidden=True
)
):
raise NotImplementedError(
f"updating unknown field not supported yet: {field_name}"
)
payment[field_name] = deepcopy(field_value)

def delete(self):
self.__payments.clear()


class Payment(Mock):
objects = PaymentQuerySet()

id = 1
description = "payment"
currency = "USD"
Expand All @@ -57,9 +109,14 @@ class Payment(Mock):
}
)

@property
def pk(self):
return self.id

def change_status(self, status, message=""):
self.status = status
self.message = message
self.save(update_fields=["status", "message"])

def get_failure_url(self):
return "http://cancel.com"
Expand All @@ -77,10 +134,58 @@ def get_purchased_items(self):
def get_success_url(self):
return "http://success.com"

def save(self, *args, update_fields=None, **kwargs):
if args or kwargs:
raise NotImplementedError(f"arguments not supported yet: {args}, {kwargs}")
if update_fields is None:
update_fields = {
field.name
for field in self._meta.get_fields(
include_parents=True, include_hidden=True
)
}
Payment.objects.filter(pk=self.pk).update(
**{field: getattr(self, field) for field in update_fields}
)

def refresh_from_db(self, *args, **kwargs):
if args or kwargs:
raise NotImplementedError(f"arguments not supported yet: {args}, {kwargs}")
payment_from_db = Payment.objects.get(pk=self.pk)
for field in self._meta.get_fields(include_parents=True, include_hidden=True):
field_value_from_db = getattr(payment_from_db, field.name)
setattr(self, field.name, field_value_from_db)

class Meta(Mock):
def get_fields(self, include_parents=True, include_hidden=False):
fields = []
for field_name in {
"id",
"description",
"currency",
"delivery",
"status",
"tax",
"token",
"total",
"captured_amount",
"variant",
"transaction_id",
"message",
"extra_data",
}:
field = Mock()
field.name = field_name
fields.append(field)
return tuple(fields)

_meta = Meta()


class TestPaypalProvider(TestCase):
def setUp(self):
self.payment = Payment()
Payment.objects.delete()
self.payment = Payment.objects.create()
self.provider = PaypalProvider(secret=SECRET, client_id=CLIENT_ID)

def test_provider_raises_redirect_needed_on_success(self):
Expand Down Expand Up @@ -171,6 +276,9 @@ def test_provider_redirects_on_success_captured_payment(

self.assertEqual(self.payment.status, PaymentStatus.CONFIRMED)
self.assertEqual(self.payment.captured_amount, self.payment.total)
self.payment.refresh_from_db()
self.assertEqual(self.payment.status, PaymentStatus.CONFIRMED)
self.assertEqual(self.payment.captured_amount, self.payment.total)

@patch("requests.post")
@patch("payments.paypal.redirect")
Expand Down Expand Up @@ -202,6 +310,9 @@ def test_provider_redirects_on_success_preauth_payment(

self.assertEqual(self.payment.status, PaymentStatus.PREAUTH)
self.assertEqual(self.payment.captured_amount, Decimal("0"))
self.payment.refresh_from_db()
self.assertEqual(self.payment.status, PaymentStatus.PREAUTH)
self.assertEqual(self.payment.captured_amount, Decimal("0"))

@patch("payments.paypal.redirect")
def test_provider_request_without_payerid_redirects_on_failure(
Expand All @@ -211,6 +322,8 @@ def test_provider_request_without_payerid_redirects_on_failure(
request.GET = {"token": "test", "PayerID": None}
self.provider.process_data(self.payment, request)
self.assertEqual(self.payment.status, PaymentStatus.REJECTED)
self.payment.refresh_from_db()
self.assertEqual(self.payment.status, PaymentStatus.REJECTED)

@patch("requests.post")
def test_provider_renews_access_token(self, mocked_post):
Expand Down
Loading