diff --git a/cart/templates/cart.html b/cart/templates/cart.html
index cae3dd4a..26be9365 100644
--- a/cart/templates/cart.html
+++ b/cart/templates/cart.html
@@ -42,7 +42,7 @@
- Check Out
+ Check Out
|
diff --git a/cart/urls.py b/cart/urls.py
index 88aa1da5..f4728195 100644
--- a/cart/urls.py
+++ b/cart/urls.py
@@ -6,9 +6,9 @@
urlpatterns = [
path(
- "checkout/to_payment",
+ "checkout/to_payment//",
CheckoutInterstitialView.as_view(),
name="checkout_interstitial_page",
),
- path("", CartView.as_view(), name="cart"),
+ path("cart//", CartView.as_view(), name="cart"),
]
diff --git a/cart/views.py b/cart/views.py
index c0c6e719..b25daff1 100644
--- a/cart/views.py
+++ b/cart/views.py
@@ -12,7 +12,7 @@
from payments import api
from payments.models import Basket
-from system_meta.models import Product
+from system_meta.models import IntegratedSystem, Product
log = logging.getLogger(__name__)
@@ -23,9 +23,10 @@ class CartView(LoginRequiredMixin, TemplateView):
template_name = "cart.html"
extra_context = {"title": "Cart", "innertitle": "Cart"}
- def get(self, request: HttpRequest) -> HttpResponse:
+ def get(self, request: HttpRequest, system_slug: str) -> HttpResponse:
"""Render the cart page."""
- basket = Basket.establish_basket(request)
+ system = IntegratedSystem.objects.get(slug=system_slug)
+ basket = Basket.establish_basket(request, system)
products = Product.objects.all()
if not request.user.is_authenticated:
@@ -57,10 +58,11 @@ class CheckoutInterstitialView(LoginRequiredMixin, TemplateView):
template_name = "checkout_interstitial.html"
- def get(self, request):
+ def get(self, request, system_slug):
"""Render the checkout interstitial page."""
try:
- checkout_payload = api.generate_checkout_payload(request)
+ system = IntegratedSystem.objects.get(slug=system_slug)
+ checkout_payload = api.generate_checkout_payload(request, system)
except ObjectDoesNotExist:
return HttpResponse("No basket")
if (
diff --git a/payments/api.py b/payments/api.py
index a7f84161..06efdafb 100644
--- a/payments/api.py
+++ b/payments/api.py
@@ -35,9 +35,9 @@
log = logging.getLogger(__name__)
-def generate_checkout_payload(request):
+def generate_checkout_payload(request, system):
"""Generate the payload to send to the payment gateway."""
- basket = Basket.establish_basket(request)
+ basket = Basket.establish_basket(request, system)
# Notes for future implementation: this used to check for
# * Blocked products (by country)
diff --git a/payments/api_test.py b/payments/api_test.py
index 3ecfa8e0..f8c0b72f 100644
--- a/payments/api_test.py
+++ b/payments/api_test.py
@@ -146,7 +146,8 @@ def create_basket(user, products):
Bootstrap a basket with a product in it for testing the discount
redemption APIs
"""
- basket = Basket(user=user)
+ integrated_system = products[0].system
+ basket = Basket(user=user, integrated_system=integrated_system)
basket.save()
basket_item = BasketItem(
@@ -414,7 +415,7 @@ def test_process_cybersource_payment_response(rf, mocker, user, products):
return_value=True,
)
create_basket(user, products)
- resp = generate_checkout_payload(generate_mocked_request(user))
+ resp = generate_checkout_payload(generate_mocked_request(user), products[0].system)
payload = resp["payload"]
payload = {
@@ -453,7 +454,7 @@ def test_process_cybersource_payment_decline_response(
)
create_basket(user, products)
- resp = generate_checkout_payload(generate_mocked_request(user))
+ resp = generate_checkout_payload(generate_mocked_request(user), products[0].system)
payload = resp["payload"]
payload = {
diff --git a/payments/factories.py b/payments/factories.py
index 8de51b87..be4cad6b 100644
--- a/payments/factories.py
+++ b/payments/factories.py
@@ -5,7 +5,7 @@
from factory.django import DjangoModelFactory
from payments import models
-from system_meta.factories import ProductFactory
+from system_meta.factories import IntegratedSystemFactory, ProductFactory
from unified_ecommerce.factories import UserFactory
FAKE = faker.Factory.create()
@@ -15,6 +15,7 @@ class BasketFactory(DjangoModelFactory):
"""Factory for Basket"""
user = SubFactory(UserFactory)
+ integrated_system = SubFactory(IntegratedSystemFactory)
class Meta:
"""Meta options for BasketFactory"""
diff --git a/payments/migrations/0004_remove_existing_baskets.py b/payments/migrations/0004_remove_existing_baskets.py
new file mode 100644
index 00000000..4e14f3e8
--- /dev/null
+++ b/payments/migrations/0004_remove_existing_baskets.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.16 on 2024-10-16 18:16
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("payments", "0003_alter_order_state"),
+ ]
+
+ def _delete_existing_baskets(apps, scheme_editor): # noqa: ARG002, N805
+ model = apps.get_model("payments", "basket")
+ model.objects.all().delete()
+
+ operations = [
+ migrations.RunPython(_delete_existing_baskets),
+ ]
diff --git a/payments/migrations/0005_basket_integrated_system_alter_basket_user.py b/payments/migrations/0005_basket_integrated_system_alter_basket_user.py
new file mode 100644
index 00000000..a7bb0241
--- /dev/null
+++ b/payments/migrations/0005_basket_integrated_system_alter_basket_user.py
@@ -0,0 +1,35 @@
+# Generated by Django 4.2.16 on 2024-10-16 18:16
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("system_meta", "0005_integratedsystem_payment_process_redirect_url"),
+ ("payments", "0004_remove_existing_baskets"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="basket",
+ name="integrated_system",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="basket",
+ to="system_meta.integratedsystem",
+ ),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name="basket",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="basket",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ]
diff --git a/payments/models.py b/payments/models.py
index e9568627..834365e4 100644
--- a/payments/models.py
+++ b/payments/models.py
@@ -14,7 +14,7 @@
from mitol.common.models import TimestampedModel
from reversion.models import Version
-from system_meta.models import Product
+from system_meta.models import IntegratedSystem, Product
from unified_ecommerce.constants import (
POST_SALE_SOURCE_REDIRECT,
TRANSACTION_TYPE_PAYMENT,
@@ -31,7 +31,10 @@
class Basket(TimestampedModel):
"""Represents a User's basket."""
- user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="basket")
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="basket")
+ integrated_system = models.ForeignKey(
+ IntegratedSystem, on_delete=models.CASCADE, related_name="basket"
+ )
def compare_to_order(self, order):
"""
@@ -57,7 +60,7 @@ def get_products(self):
return [item.product for item in self.basket_items.all()]
@staticmethod
- def establish_basket(request):
+ def establish_basket(request, integrated_system: IntegratedSystem):
"""
Get or create the user's basket.
@@ -66,15 +69,22 @@ def establish_basket(request):
system (IntegratedSystem): The system to associate with the basket.
"""
user = request.user
- (basket, is_new) = Basket.objects.filter(user=user).get_or_create(
- defaults={"user": user}
- )
+ (basket, is_new) = Basket.objects.filter(
+ user=user, integrated_system=integrated_system
+ ).get_or_create(defaults={"user": user, "integrated_system": integrated_system})
if is_new:
basket.save()
return basket
+ constraints = [
+ models.UniqueConstraint(
+ fields=["user", "integrated_system"],
+ name="unique_user_integrated_system",
+ ),
+ ]
+
class BasketItem(TimestampedModel):
"""Represents one or more products in a user's basket."""
diff --git a/payments/views/v0/__init__.py b/payments/views/v0/__init__.py
index 2a09c968..27fe1b43 100644
--- a/payments/views/v0/__init__.py
+++ b/payments/views/v0/__init__.py
@@ -106,7 +106,7 @@ def create_basket_from_product(request, system_slug: str, sku: str):
Response: HTTP response
"""
system = IntegratedSystem.objects.get(slug=system_slug)
- basket = Basket.establish_basket(request)
+ basket = Basket.establish_basket(request, system)
quantity = request.data.get("quantity", 1)
checkout = request.data.get("checkout", False)
@@ -123,7 +123,7 @@ def create_basket_from_product(request, system_slug: str, sku: str):
basket.refresh_from_db()
if checkout:
- return redirect("checkout_interstitial_page")
+ return redirect("checkout_interstitial_page", system_slug=system.slug)
return Response(
BasketWithProductSerializer(basket).data,
@@ -139,14 +139,18 @@ def create_basket_from_product(request, system_slug: str, sku: str):
)
@api_view(["DELETE"])
@permission_classes([IsAuthenticated])
-def clear_basket(request):
+def clear_basket(request, system_slug: str):
"""
Clear the basket for the current user.
+ Args:
+ system_slug (str): system slug
+
Returns:
Response: HTTP response
"""
- basket = Basket.establish_basket(request)
+ system = IntegratedSystem.objects.get(slug=system_slug)
+ basket = Basket.establish_basket(request, system)
basket.delete()
@@ -185,7 +189,8 @@ def start_checkout(self, request):
ultimately POST to the actual payment processor.
"""
try:
- payload = api.generate_checkout_payload(request)
+ system = IntegratedSystem.objects.get(slug=self.kwargs["system_slug"])
+ payload = api.generate_checkout_payload(request, system)
except ObjectDoesNotExist:
return Response("No basket", status=status.HTTP_406_NOT_ACCEPTABLE)
diff --git a/payments/views/v0/urls.py b/payments/views/v0/urls.py
index 065c63bb..1755ee4f 100644
--- a/payments/views/v0/urls.py
+++ b/payments/views/v0/urls.py
@@ -19,7 +19,9 @@
router.register(r"orders/history", OrderHistoryViewSet, basename="orderhistory_api")
-router.register(r"checkout", CheckoutApiViewSet, basename="checkout")
+router.register(
+ r"checkout/r''", CheckoutApiViewSet, basename="checkout"
+)
urlpatterns = [
path(
@@ -28,7 +30,7 @@
name="create_from_product",
),
path(
- "baskets/clear/",
+ "baskets/clear//",
clear_basket,
name="clear_basket",
),