Skip to content

Commit

Permalink
Separate baskets/checkout for each integrated system and user (#161)
Browse files Browse the repository at this point in the history
* Seems to be working

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add operation

* Add mgiration operation to remove baskets

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ruff

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
cp-at-mit and pre-commit-ci[bot] authored Oct 19, 2024
1 parent dd4f179 commit 4d1e86c
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 27 deletions.
2 changes: 1 addition & 1 deletion cart/templates/cart.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<tfoot>
<tr>
<td colspan="4">
<a href="{% url 'checkout_interstitial_page' %}" class="btn btn-primary float-end">Check Out</a>
<a href="{% url 'checkout_interstitial_page' system_slug=basket.integrated_system.slug %}" class="btn btn-primary float-end">Check Out</a>
</td>
</tr>
</tfoot>
Expand Down
4 changes: 2 additions & 2 deletions cart/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

urlpatterns = [
path(
"checkout/to_payment",
"checkout/to_payment/<str:system_slug>/",
CheckoutInterstitialView.as_view(),
name="checkout_interstitial_page",
),
path("", CartView.as_view(), name="cart"),
path("cart/<str:system_slug>/", CartView.as_view(), name="cart"),
]
12 changes: 7 additions & 5 deletions cart/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand Down Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions payments/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions payments/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down
3 changes: 2 additions & 1 deletion payments/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -15,6 +15,7 @@ class BasketFactory(DjangoModelFactory):
"""Factory for Basket"""

user = SubFactory(UserFactory)
integrated_system = SubFactory(IntegratedSystemFactory)

class Meta:
"""Meta options for BasketFactory"""
Expand Down
17 changes: 17 additions & 0 deletions payments/migrations/0004_remove_existing_baskets.py
Original file line number Diff line number Diff line change
@@ -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),
]
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
22 changes: 16 additions & 6 deletions payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
"""
Expand All @@ -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.
Expand All @@ -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."""
Expand Down
15 changes: 10 additions & 5 deletions payments/views/v0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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,
Expand All @@ -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()

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions payments/views/v0/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'<str:system_slug>'", CheckoutApiViewSet, basename="checkout"
)

urlpatterns = [
path(
Expand All @@ -28,7 +30,7 @@
name="create_from_product",
),
path(
"baskets/clear/",
"baskets/clear/<str:system_slug>/",
clear_basket,
name="clear_basket",
),
Expand Down

0 comments on commit 4d1e86c

Please sign in to comment.