Skip to content

Commit

Permalink
add AbstractOrder.return_order
Browse files Browse the repository at this point in the history
  • Loading branch information
radekholy24 authored and PetrDlouhy committed Apr 5, 2024
1 parent d3dbc6f commit bceb031
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 4 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ django-plans changelog

1.1.0 (unreleased)
------------------
* Add `AbstractUserPlan.reduce_account()`
* Add `AbstractOrder.return_order()`

1.0.6
-----
Expand Down
10 changes: 9 additions & 1 deletion plans/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ def make_order_completed(modeladmin, request, queryset):
make_order_completed.short_description = _("Make selected orders completed")


def make_order_returned(modeladmin, request, queryset):
for order in queryset:
order.return_order()


make_order_returned.short_description = _("Make selected orders returned")


def make_order_invoice(modeladmin, request, queryset):
for order in queryset:
if (
Expand Down Expand Up @@ -192,7 +200,7 @@ class OrderAdmin(admin.ModelAdmin):
)
readonly_fields = ("created", "updated_at")
list_display_links = list_display
actions = [make_order_completed, make_order_invoice]
actions = [make_order_completed, make_order_returned, make_order_invoice]
inlines = (InvoiceInline,)

def queryset(self, request):
Expand Down
28 changes: 28 additions & 0 deletions plans/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,34 @@ def complete_order(self):
else:
return False

def return_order(self):
if self.status != self.STATUS.RETURNED:
if self.status == self.STATUS.COMPLETED:
if self.pricing is not None:
extended_from = self.plan_extended_from
if extended_from is None:
extended_from = self.completed
# Should never happen, but make sure we reduce for the same number of days as we extended.
if (
self.plan_extended_until is None
or extended_from is None
or self.plan_extended_until - extended_from
!= timedelta(days=self.pricing.period)
):
raise ValueError(
f"Invalid order state: completed={self.completed}, "
f"plan_extended_from={self.plan_extended_from}, "
f"plan_extended_until={self.plan_extended_until}, "
f"pricing.period={self.pricing.period}"
)
self.user.userplan.reduce_account(self.pricing)
elif self.status != self.STATUS.NOT_VALID:
raise ValueError(
f"Cannot return order with status other than COMPLETED and NOT_VALID: {self.status}"
)
self.status = self.STATUS.RETURNED
self.save()

def get_invoices_proforma(self):
return AbstractInvoice.get_concrete_model().proforma.filter(order=self)

Expand Down
234 changes: 232 additions & 2 deletions plans/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import random
from datetime import date, timedelta
import re
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from io import StringIO
from unittest import mock
Expand All @@ -14,7 +15,7 @@
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.management import call_command
from django.db import transaction
from django.db.models import Q
from django.db.models import Exists, OuterRef, Q
from django.test import RequestFactory, TestCase, TransactionTestCase, override_settings
from django.urls import reverse
from django_concurrent_tests.helpers import call_concurrently
Expand Down Expand Up @@ -857,6 +858,235 @@ def test_order_complete_order_completed(self):
)
self.assertFalse(order.complete_order())

def test_return_order_new(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.filter(
plan=u.userplan.plan, pricing__period__gt=0
).first()
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)

with self.assertRaisesRegex(
ValueError,
rf"^Cannot return order with status other than COMPLETED and NOT_VALID: "
rf"{re.escape(str(Order.STATUS.NEW))}$",
):
order.return_order()
self.assertEqual(order.status, Order.STATUS.NEW)
self.assertEqual(u.userplan.plan, plan_pricing.plan)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50))

def test_return_order_completed(self):
u = User.objects.get(username="test1")
u.userplan.plan = Plan.objects.filter(planpricing__isnull=True).first()
u.userplan.expire = None
u.userplan.save()
plan_pricing = PlanPricing.objects.filter(pricing__period__gt=0).first()
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_pricing.plan)
self.assertEqual(u.userplan.expire, date.today())

def test_return_order_completed_then_same_plan(self):
u = User.objects.get(username="test1")
u.userplan.plan = (
Plan.objects.annotate(
planpricing_pricing_period_eq_30_exists=Exists(
PlanPricing.objects.filter(plan=OuterRef("pk"), pricing__period=30)
),
planpricing_pricing_period_gt_30_exists=Exists(
PlanPricing.objects.filter(
plan=OuterRef("pk"), pricing__period__gt=30
)
),
)
.filter(
planpricing_pricing_period_eq_30_exists=True,
planpricing_pricing_period_gt_30_exists=True,
)
.first()
)
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.filter(
plan=u.userplan.plan, pricing__period__gt=30
).first()
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()
plan_pricing_then = PlanPricing.objects.get(
plan=plan_pricing.plan, pricing__period=30
)
order_then = Order.objects.create(
user=u,
pricing=plan_pricing_then.pricing,
amount=100,
plan=plan_pricing_then.plan,
status=Order.STATUS.NEW,
)
order_then.complete_order()

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_pricing_then.plan)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50 + 30))

def test_return_order_completed_then_paid_plan(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.get(plan=u.userplan.plan, pricing__period=30)
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()
plan_pricing_then = (
PlanPricing.objects.exclude(plan=plan_pricing.plan)
.filter(pricing__period=365)
.first()
)
order_then = Order.objects.create(
user=u,
pricing=plan_pricing_then.pricing,
amount=100,
plan=plan_pricing_then.plan,
status=Order.STATUS.NEW,
)
with freeze_time(datetime.combine(u.userplan.expire, time())):
order_then.complete_order()

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_pricing_then.plan)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50 + 365))

def test_return_order_completed_then_free_plan(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.get(plan=u.userplan.plan, pricing__period=30)
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()
plan_then = (
Plan.objects.exclude(pk=plan_pricing.plan.pk)
.filter(planpricing__isnull=True)
.first()
)
u.userplan.extend_account(plan_then, None)

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_then)
self.assertIsNone(u.userplan.expire)

def test_return_order_not_valid(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_user = u.userplan.plan
plan_pricing = (
PlanPricing.objects.exclude(plan=u.userplan.plan)
.filter(pricing__period__gt=0)
.first()
)
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_user)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50))

def test_return_order_canceled(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.filter(
plan=u.userplan.plan, pricing__period__gt=0
).first()
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.CANCELED,
)

with self.assertRaisesRegex(
ValueError,
rf"^Cannot return order with status other than COMPLETED and NOT_VALID: "
rf"{re.escape(str(Order.STATUS.CANCELED))}$",
):
order.return_order()
self.assertEqual(order.status, Order.STATUS.CANCELED)
self.assertEqual(u.userplan.plan, plan_pricing.plan)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50))

def test_return_order_returned(self):
u = User.objects.get(username="test1")
u.userplan.expire = date.today() + timedelta(days=50)
u.userplan.save()
plan_pricing = PlanPricing.objects.filter(
plan=u.userplan.plan, pricing__period__gt=0
).first()
order = Order.objects.create(
user=u,
pricing=plan_pricing.pricing,
amount=100,
plan=plan_pricing.plan,
status=Order.STATUS.NEW,
)
order.complete_order()
order.return_order()

order.return_order()

self.assertEqual(order.status, Order.STATUS.RETURNED)
self.assertEqual(u.userplan.plan, plan_pricing.plan)
self.assertEqual(u.userplan.expire, date.today() + timedelta(days=50))

def test_amount_taxed_none(self):
o = Order()
o.amount = Decimal(123)
Expand Down

0 comments on commit bceb031

Please sign in to comment.