feat: improved order payment lifecycle
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-01-16 17:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("shop", "0004_shopsettings_tpv_domain"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="order",
|
||||||
|
options={
|
||||||
|
"ordering": ("creation_date",),
|
||||||
|
"verbose_name": "pedido",
|
||||||
|
"verbose_name_plural": "pedidos",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="order",
|
||||||
|
name="amount_paid",
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
default=Decimal("0.00"),
|
||||||
|
max_digits=11,
|
||||||
|
verbose_name="cantidad pagada",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="order",
|
||||||
|
name="from_cart",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="shop.cart",
|
||||||
|
verbose_name="carrito de origen de pedido",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -429,6 +429,13 @@ class Order(TimestampedModel):
|
|||||||
max_length=32, blank=True, verbose_name=_("teléfono de contacto")
|
max_length=32, blank=True, verbose_name=_("teléfono de contacto")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
amount_paid = models.DecimalField(
|
||||||
|
default=Decimal("0.00"),
|
||||||
|
max_digits=11,
|
||||||
|
decimal_places=2,
|
||||||
|
verbose_name=_("cantidad pagada"),
|
||||||
|
)
|
||||||
|
|
||||||
shipping_method = models.ForeignKey(
|
shipping_method = models.ForeignKey(
|
||||||
"shop.ShippingMethod",
|
"shop.ShippingMethod",
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -437,6 +444,14 @@ class Order(TimestampedModel):
|
|||||||
verbose_name=_("método de envío"),
|
verbose_name=_("método de envío"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from_cart = models.ForeignKey(
|
||||||
|
"shop.Cart",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("carrito de origen de pedido"),
|
||||||
|
)
|
||||||
|
|
||||||
def calculate_total_from_lines(self):
|
def calculate_total_from_lines(self):
|
||||||
self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get(
|
self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get(
|
||||||
"base_total"
|
"base_total"
|
||||||
|
|||||||
+29
-6
@@ -7,6 +7,7 @@ from decimal import Decimal
|
|||||||
import pyDes
|
import pyDes
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.text import gettext_lazy as _
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
@@ -20,8 +21,10 @@ from shop.models import (
|
|||||||
ProductBatch,
|
ProductBatch,
|
||||||
ProductPrice,
|
ProductPrice,
|
||||||
ShippingMethod,
|
ShippingMethod,
|
||||||
|
CartItem,
|
||||||
)
|
)
|
||||||
from shop.settings import ERROR_CODES
|
from shop.settings import ERROR_CODES
|
||||||
|
from shop.signals import clear_cart
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ def create_order_from_cart(
|
|||||||
shipping_method=shipping_method,
|
shipping_method=shipping_method,
|
||||||
user=cart.user,
|
user=cart.user,
|
||||||
email=email,
|
email=email,
|
||||||
|
from_cart=cart,
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in cart.items.all():
|
for item in cart.items.all():
|
||||||
@@ -261,15 +265,34 @@ def validate_expiry_date(expiry_date: str):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def pay_order(order: Order, amount_paid: Decimal):
|
def update_order_payment_status(order: Order):
|
||||||
with transaction.atomic() as tx:
|
if order.amount_paid >= order.total:
|
||||||
|
order.status = Order.Statuses.STATUS_PAID
|
||||||
|
elif order.status == Order.Statuses.STATUS_PAID and order.amount_paid == Decimal(
|
||||||
|
"0.00"
|
||||||
|
):
|
||||||
|
order.status = Order.Statuses.STATUS_RETURNED
|
||||||
|
else:
|
||||||
|
order.status = Order.Statuses.STATUS_PENDING
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_cart_items_from_order(order):
|
||||||
|
if order.status == Order.Statuses.STATUS_PAID and order.from_cart:
|
||||||
|
CartItem.objects.filter(cart=order.from_cart).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def add_payment_to_order(order: Order, amount):
|
||||||
|
with transaction.atomic():
|
||||||
payment = Payment.objects.create(
|
payment = Payment.objects.create(
|
||||||
amount=amount_paid,
|
amount=amount,
|
||||||
order=order,
|
order=order,
|
||||||
user=order.user,
|
user=order.user,
|
||||||
method=Payment.MethodChoices.REDSYS,
|
method=Payment.MethodChoices.REDSYS,
|
||||||
)
|
)
|
||||||
|
order.amount_paid += payment.amount
|
||||||
|
|
||||||
if amount_paid >= order.total:
|
update_order_payment_status(order)
|
||||||
order.status = Order.Statuses.STATUS_PAID
|
|
||||||
order.save()
|
delete_cart_items_from_order(order)
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from shop.models import Order
|
from shop.models import Order
|
||||||
from shop.utils import pay_order, validate_payment_for_order
|
from shop.utils import validate_payment_for_order, add_payment_to_order
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@@ -12,7 +12,7 @@ def webhook(request, uuid):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
amount_paid = validate_payment_for_order(request, order)
|
amount_paid = validate_payment_for_order(request, order)
|
||||||
pay_order(order, amount_paid)
|
add_payment_to_order(order, amount_paid)
|
||||||
|
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class CartItemForm(forms.Form):
|
|||||||
<label for="quantity">{% translate 'Cantidad' %}</label>
|
<label for="quantity">{% translate 'Cantidad' %}</label>
|
||||||
<input id="quantity" type="number" name="quantity" value="1">
|
<input id="quantity" type="number" name="quantity" value="1">
|
||||||
"""
|
"""
|
||||||
|
|
||||||
product = forms.IntegerField(widget=widgets.HiddenInput)
|
product = forms.IntegerField(widget=widgets.HiddenInput)
|
||||||
quantity = forms.IntegerField()
|
quantity = forms.IntegerField()
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ class TestManifest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.settings = WebSettings.load()
|
self.settings = WebSettings.load()
|
||||||
|
|
||||||
with open(os.path.join(settings.BASE_DIR, 'web', 'tests', 'images', 'trolley.png'), 'rb') as f:
|
with open(
|
||||||
file_128 = SimpleUploadedFile('logo_128.png', f.read())
|
os.path.join(settings.BASE_DIR, "web", "tests", "images", "trolley.png"),
|
||||||
|
"rb",
|
||||||
|
) as f:
|
||||||
|
file_128 = SimpleUploadedFile("logo_128.png", f.read())
|
||||||
self.settings.logo_128 = file_128
|
self.settings.logo_128 = file_128
|
||||||
file_240 = SimpleUploadedFile('logo_240.png', f.read())
|
file_240 = SimpleUploadedFile("logo_240.png", f.read())
|
||||||
self.settings.logo_240 = file_240
|
self.settings.logo_240 = file_240
|
||||||
self.settings.logo = file_240
|
self.settings.logo = file_240
|
||||||
self.settings.save()
|
self.settings.save()
|
||||||
|
|||||||
@@ -4,14 +4,23 @@ from django.contrib.auth.models import User
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from shop.models import Cart, CartItem, Order, CustomerAddress, ShippingMethod, ShopSettings
|
from shop.models import (
|
||||||
|
Cart,
|
||||||
|
CartItem,
|
||||||
|
Order,
|
||||||
|
CustomerAddress,
|
||||||
|
ShippingMethod,
|
||||||
|
ShopSettings,
|
||||||
|
)
|
||||||
from shop.tests.mixins import CreateProductsMixin
|
from shop.tests.mixins import CreateProductsMixin
|
||||||
|
|
||||||
|
|
||||||
class TestOrders(TestCase, CreateProductsMixin):
|
class TestOrders(TestCase, CreateProductsMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.product = self.create_product()
|
self.product = self.create_product()
|
||||||
self.user = User.objects.create_user("anakin", "anakin@skywalker.com", "ihatesand")
|
self.user = User.objects.create_user(
|
||||||
|
"anakin", "anakin@skywalker.com", "ihatesand"
|
||||||
|
)
|
||||||
self.cart_for_user = self.create_cart(user=self.user)
|
self.cart_for_user = self.create_cart(user=self.user)
|
||||||
self.anonymous_cart = self.create_cart()
|
self.anonymous_cart = self.create_cart()
|
||||||
self.address = CustomerAddress.objects.create(
|
self.address = CustomerAddress.objects.create(
|
||||||
|
|||||||
Reference in New Issue
Block a user