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", ),