aboutsummaryrefslogtreecommitdiffstats
path: root/alphabetlearning/payments
diff options
context:
space:
mode:
Diffstat (limited to 'alphabetlearning/payments')
-rw-r--r--alphabetlearning/payments/__init__.py0
-rw-r--r--alphabetlearning/payments/admin.py38
-rw-r--r--alphabetlearning/payments/apps.py9
-rw-r--r--alphabetlearning/payments/migrations/0001_initial.py103
-rw-r--r--alphabetlearning/payments/migrations/0002_subscriptionplan_and_more.py36
-rw-r--r--alphabetlearning/payments/migrations/0003_product_price.py53
-rw-r--r--alphabetlearning/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py18
-rw-r--r--alphabetlearning/payments/migrations/0005_remove_subscriptionplan_stripe_plan_id.py17
-rw-r--r--alphabetlearning/payments/migrations/0006_subscription_plan.py24
-rw-r--r--alphabetlearning/payments/migrations/0007_remove_cartitem_quantity_and_more.py23
-rw-r--r--alphabetlearning/payments/migrations/__init__.py0
-rw-r--r--alphabetlearning/payments/models.py71
-rw-r--r--alphabetlearning/payments/signals.py40
-rw-r--r--alphabetlearning/payments/tests/__init__.py0
-rw-r--r--alphabetlearning/payments/tests/conftest.py6
-rw-r--r--alphabetlearning/payments/tests/test_models.py116
-rw-r--r--alphabetlearning/payments/tests/test_views.py19
-rw-r--r--alphabetlearning/payments/urls.py22
-rw-r--r--alphabetlearning/payments/views.py96
19 files changed, 691 insertions, 0 deletions
diff --git a/alphabetlearning/payments/__init__.py b/alphabetlearning/payments/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/alphabetlearning/payments/__init__.py
diff --git a/alphabetlearning/payments/admin.py b/alphabetlearning/payments/admin.py
new file mode 100644
index 0000000..f6a00a2
--- /dev/null
+++ b/alphabetlearning/payments/admin.py
@@ -0,0 +1,38 @@
+from django.contrib import admin
+
+from .models import CartItem
+from .models import Price
+from .models import Product
+from .models import ShoppingCart
+from .models import Subscription
+from .models import SubscriptionPlan
+
+
+class PriceInlineAdmin(admin.TabularInline):
+ model = Price
+ extra = 0
+
+class ProductAdmin(admin.ModelAdmin):
+ inlines = [PriceInlineAdmin]
+
+class SubscriptionPlanAdmin(admin.ModelAdmin):
+ list_display = ('name', 'price', 'description', 'allowed_downloads')
+
+
+class ShoppingCartAdmin(admin.ModelAdmin):
+ list_display = ('user', 'created_at', 'updated_at')
+
+
+class CartItemAdmin(admin.ModelAdmin):
+ list_display = ('cart', 'resource', 'quantity', 'added_at')
+
+
+class SubscriptionAdmin(admin.ModelAdmin):
+ list_display = ('user', 'is_active', 'start_date', 'end_date')
+
+
+admin.site.register(Product, ProductAdmin)
+admin.site.register(SubscriptionPlan)
+admin.site.register(ShoppingCart)
+admin.site.register(CartItem)
+admin.site.register(Subscription)
diff --git a/alphabetlearning/payments/apps.py b/alphabetlearning/payments/apps.py
new file mode 100644
index 0000000..e9b0a47
--- /dev/null
+++ b/alphabetlearning/payments/apps.py
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class PaymentsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "alphabetlearning.payments"
+
+ def ready(self):
+ import alphabetlearning.payments.signals # noqa: F401
diff --git a/alphabetlearning/payments/migrations/0001_initial.py b/alphabetlearning/payments/migrations/0001_initial.py
new file mode 100644
index 0000000..33b7602
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0001_initial.py
@@ -0,0 +1,103 @@
+# Generated by Django 5.0.4 on 2024-09-03 19:21
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("resources", "0019_alter_pdfpagesnapshot_options_and_more"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ShoppingCart",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Subscription",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("is_active", models.BooleanField(default=False)),
+ ("start_date", models.DateTimeField(blank=True, null=True)),
+ ("end_date", models.DateTimeField(blank=True, null=True)),
+ (
+ "stripe_subscription_id",
+ models.CharField(blank=True, max_length=255, null=True),
+ ),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="CartItem",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("quantity", models.PositiveIntegerField(default=1)),
+ ("added_at", models.DateTimeField(auto_now_add=True)),
+ (
+ "resource",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="resources.resource",
+ ),
+ ),
+ (
+ "cart",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="items",
+ to="payments.shoppingcart",
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("cart", "resource")},
+ },
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0002_subscriptionplan_and_more.py b/alphabetlearning/payments/migrations/0002_subscriptionplan_and_more.py
new file mode 100644
index 0000000..cab49b5
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0002_subscriptionplan_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 5.0.4 on 2024-09-03 19:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SubscriptionPlan",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255)),
+ ("price", models.DecimalField(decimal_places=2, max_digits=6)),
+ ("description", models.TextField()),
+ ("allowed_downloads", models.PositiveIntegerField()),
+ ("stripe_plan_id", models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name="subscription",
+ name="stripe_subscription_id",
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0003_product_price.py b/alphabetlearning/payments/migrations/0003_product_price.py
new file mode 100644
index 0000000..b12d5dc
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0003_product_price.py
@@ -0,0 +1,53 @@
+# Generated by Django 5.0.4 on 2024-09-04 19:01
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0002_subscriptionplan_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Product",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255)),
+ ("stripe_product_id", models.CharField(max_length=100)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Price",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("price", models.IntegerField(default=0)),
+ ("stripe_product_id", models.CharField(max_length=100)),
+ (
+ "product",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="payments.product",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py b/alphabetlearning/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py
new file mode 100644
index 0000000..e5a339f
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-09-04 19:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0003_product_price"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="price",
+ old_name="stripe_product_id",
+ new_name="stripe_price_id",
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0005_remove_subscriptionplan_stripe_plan_id.py b/alphabetlearning/payments/migrations/0005_remove_subscriptionplan_stripe_plan_id.py
new file mode 100644
index 0000000..1e642b4
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0005_remove_subscriptionplan_stripe_plan_id.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0.4 on 2024-09-08 19:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0004_rename_stripe_product_id_price_stripe_price_id"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="subscriptionplan",
+ name="stripe_plan_id",
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0006_subscription_plan.py b/alphabetlearning/payments/migrations/0006_subscription_plan.py
new file mode 100644
index 0000000..f54b5f9
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0006_subscription_plan.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.4 on 2024-09-08 20:21
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0005_remove_subscriptionplan_stripe_plan_id"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="subscription",
+ name="plan",
+ field=models.ForeignKey(
+ default=1,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="payments.subscriptionplan",
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/0007_remove_cartitem_quantity_and_more.py b/alphabetlearning/payments/migrations/0007_remove_cartitem_quantity_and_more.py
new file mode 100644
index 0000000..e976059
--- /dev/null
+++ b/alphabetlearning/payments/migrations/0007_remove_cartitem_quantity_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.4 on 2024-09-14 14:40
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('payments', '0006_subscription_plan'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='cartitem',
+ name='quantity',
+ ),
+ migrations.AddConstraint(
+ model_name='subscription',
+ constraint=models.UniqueConstraint(fields=('user', 'plan'), name='unique_user_plan'),
+ ),
+ ]
diff --git a/alphabetlearning/payments/migrations/__init__.py b/alphabetlearning/payments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/alphabetlearning/payments/migrations/__init__.py
diff --git a/alphabetlearning/payments/models.py b/alphabetlearning/payments/models.py
new file mode 100644
index 0000000..80d93c2
--- /dev/null
+++ b/alphabetlearning/payments/models.py
@@ -0,0 +1,71 @@
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+
+from alphabetlearning.resources.models import Resource
+
+
+class Product(models.Model):
+ name = models.CharField(max_length=255)
+ stripe_product_id = models.CharField(max_length=100)
+
+
+class Price(models.Model):
+ product = models.ForeignKey(Product, on_delete=models.CASCADE)
+ price = models.IntegerField(default=0)
+ stripe_price_id = models.CharField(max_length=100)
+
+ def get_display_price(self):
+ return "{0:.2f}".format(self.price / 100)
+
+
+class ShoppingCart(models.Model):
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"Shopping cart for {self.user.email}"
+
+ def add_resource(self, resource: Resource):
+ if CartItem.objects.filter(cart=self, resource=resource).exists():
+ raise ValidationError(f"{resource.name} is already in your shopping cart.")
+ item, created = CartItem.objects.get_or_create(
+ cart=self,
+ resource=resource,
+ )
+ if not created:
+ item.save()
+
+
+class CartItem(models.Model):
+ cart = models.ForeignKey(ShoppingCart, on_delete=models.CASCADE, related_name="items")
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
+ added_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ("cart", "resource")
+
+ def __str__(self):
+ return f"{self.resource.name} in {self.cart.user.username}'s cart"
+
+
+class SubscriptionPlan(models.Model):
+ name = models.CharField(max_length=255)
+ price = models.DecimalField(max_digits=6, decimal_places=2)
+ description = models.TextField()
+ allowed_downloads = models.PositiveIntegerField()
+
+
+class Subscription(models.Model):
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ plan = models.ForeignKey(SubscriptionPlan, on_delete=models.CASCADE)
+ is_active = models.BooleanField(default=False)
+ start_date = models.DateTimeField(null=True, blank=True)
+ end_date = models.DateTimeField(null=True, blank=True)
+
+ class Meta:
+ constraints = [models.UniqueConstraint(fields=["user", "plan"], name="unique_user_plan")]
+
+ def __str__(self):
+ return f"Subscription for {self.user.username}"
diff --git a/alphabetlearning/payments/signals.py b/alphabetlearning/payments/signals.py
new file mode 100644
index 0000000..50d988b
--- /dev/null
+++ b/alphabetlearning/payments/signals.py
@@ -0,0 +1,40 @@
+from datetime import timedelta
+
+from allauth.account.signals import user_signed_up
+from django.db import transaction
+from django.dispatch import receiver
+from django.utils import timezone
+
+from .models import ShoppingCart
+from .models import Subscription
+from .models import SubscriptionPlan
+
+
+@receiver(user_signed_up)
+def assign_default_subscription(sender, request, user, **kwargs):
+ with transaction.atomic():
+ # Get or create the free plan subscription
+ free_plan, _ = SubscriptionPlan.objects.get_or_create(
+ name="Free Plan",
+ defaults={
+ "price": 0,
+ "description": "Free plan description",
+ "allowed_downloads": 10,
+ },
+ )
+
+ # Create a SubscriptionPlan for the new user
+ Subscription.objects.create(
+ user=user,
+ plan=free_plan,
+ is_active=True,
+ start_date=timezone.now(),
+ end_date=timezone.now() + timedelta(days=365), # Example: 30 days
+ )
+
+
+@receiver(user_signed_up)
+def assign_user_a_shopping_cart(sender, request, user, **kwargs):
+ with transaction.atomic():
+ # Create a ShoppingCart for the new user
+ ShoppingCart.objects.create(user=user)
diff --git a/alphabetlearning/payments/tests/__init__.py b/alphabetlearning/payments/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/alphabetlearning/payments/tests/__init__.py
diff --git a/alphabetlearning/payments/tests/conftest.py b/alphabetlearning/payments/tests/conftest.py
new file mode 100644
index 0000000..7919170
--- /dev/null
+++ b/alphabetlearning/payments/tests/conftest.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture
+def user_data():
+ return {"email": "testuser@example.com", "password": "testpassword123"}
diff --git a/alphabetlearning/payments/tests/test_models.py b/alphabetlearning/payments/tests/test_models.py
new file mode 100644
index 0000000..8c509f5
--- /dev/null
+++ b/alphabetlearning/payments/tests/test_models.py
@@ -0,0 +1,116 @@
+import pytest
+from allauth.account.signals import user_signed_up
+from django.contrib.auth import get_user_model
+from django.core.exceptions import ValidationError
+from django.db import IntegrityError
+from django.test import RequestFactory
+
+from alphabetlearning.payments.models import ShoppingCart
+from alphabetlearning.payments.models import Subscription
+from alphabetlearning.payments.models import SubscriptionPlan
+
+User = get_user_model()
+
+
+@pytest.fixture
+def user_data():
+ return {"email": "testuser@example.com", "password": "testpassword123"}
+
+
+@pytest.mark.django_db
+def test_subscription_user_unique():
+ # Ensure the free plan exists
+ free_plan, _ = SubscriptionPlan.objects.get_or_create(
+ name="Free Plan",
+ defaults={
+ "price": 0,
+ "description": "Free plan description",
+ "allowed_downloads": 10,
+ },
+ )
+
+ # Create a new user
+ user_data = {"email": "testuser@example.com", "password": "testpassword123"}
+ user = User.objects.create_user(**user_data) # type: ignore
+
+ # Create a subscription for the user
+ Subscription.objects.create(user=user, plan=free_plan)
+
+ # Try to create another subscription for the same user
+ with pytest.raises(IntegrityError):
+ Subscription.objects.create(user=user, plan=free_plan)
+
+
+@pytest.mark.django_db
+def test_user_signup_assigns_free_subscription(user_data):
+ # Ensure the free plan exists
+ free_plan, _ = SubscriptionPlan.objects.get_or_create(
+ name="Free Plan",
+ defaults={
+ "price": 0,
+ "description": "Free plan description",
+ "allowed_downloads": 10,
+ },
+ ) # Create a new user
+ user = User.objects.create_user(**user_data) # type: ignore
+ # Manually trigger the user_signed_up signal
+ request = RequestFactory().get("/")
+ user_signed_up.send(sender=user.__class__, request=request, user=user)
+
+ # Check if a SubscriptionPlan was created for the user
+ subscription = user.subscription
+ assert subscription is not None
+
+ # Check if the assigned plan is the free plan
+ assert subscription.plan == free_plan
+
+ # Additional assertions can be added here to check other properties
+ # of the SubscriptionPlan or Subscription as needed
+
+
+@pytest.mark.django_db
+def test_shopping_cart_is_created_when_user_is_created(user_data):
+ user = User.objects.create_user(**user_data) # type: ignore
+ request = RequestFactory().get("/")
+ user_signed_up.send(sender=user.__class__, request=request, user=user)
+ shopping_cart = ShoppingCart.objects.get(user=user)
+ assert shopping_cart is not None
+ assert str(shopping_cart) == "Shopping cart for {}".format(user.email)
+
+
+# When the user adds a `Resource` to their cart, create a new `CartItem` instance and associate it with the user's `ShoppingCart` and the selected `Resource`.
+
+
+@pytest.mark.django_db
+def test_cart_item_is_created_when_resource_is_added_to_cart(user_data, resource):
+ user = User.objects.create_user(**user_data) # type: ignore
+ request = RequestFactory().get("/")
+ user_signed_up.send(sender=user.__class__, request=request, user=user)
+ users_cart = ShoppingCart.objects.get(user=user)
+ users_cart.add_resource(resource)
+ assert users_cart.items.count() == 1 # type: ignore
+
+
+@pytest.mark.django_db
+def test_cannot_add_the_same_resource_to_cart_twice(user_data, resource):
+ user = User.objects.create_user(**user_data) # type: ignore
+ request = RequestFactory().get("/")
+ user_signed_up.send(sender=user.__class__, request=request, user=user)
+ users_cart = ShoppingCart.objects.get(user=user)
+ users_cart.add_resource(resource)
+ assert users_cart.items.count() == 1 # type: ignore
+ with pytest.raises(ValidationError):
+ users_cart.add_resource(resource)
+
+
+@pytest.mark.django_db
+def test_can_add_multiple_different_items_to_cart(user_data, resources):
+ user = User.objects.create_user(**user_data) # type: ignore
+ request = RequestFactory().get("/")
+ user_signed_up.send(sender=user.__class__, request=request, user=user)
+ users_cart = ShoppingCart.objects.get(user=user)
+ users_cart.add_resource(resources[0])
+ users_cart.add_resource(resources[1])
+ users_cart.add_resource(resources[2])
+ users_cart.add_resource(resources[3])
+ assert users_cart.items.count() == 4 # type: ignore
diff --git a/alphabetlearning/payments/tests/test_views.py b/alphabetlearning/payments/tests/test_views.py
new file mode 100644
index 0000000..7d0f8b5
--- /dev/null
+++ b/alphabetlearning/payments/tests/test_views.py
@@ -0,0 +1,19 @@
+import pytest
+from django.urls import reverse
+
+
+@pytest.mark.django_db
+def test_cart_view(client, user):
+ url = reverse("payments:cart_detail")
+ client.force_login(user)
+ response = client.get(url)
+ assert response.status_code == 200
+ assert "My basket" in str(response.content)
+
+
+@pytest.mark.django_db
+def test_add_resource_to_cart(client, resource, user):
+ url = reverse("payments:add_to_cart", kwargs={"resource_id": resource.id})
+ client.force_login(user)
+ response = client.get(url)
+ assert response.status_code == 200
diff --git a/alphabetlearning/payments/urls.py b/alphabetlearning/payments/urls.py
new file mode 100644
index 0000000..52e2451
--- /dev/null
+++ b/alphabetlearning/payments/urls.py
@@ -0,0 +1,22 @@
+from django.urls import path
+
+from . import views
+from .views import CancelView
+from .views import SuccessView
+
+app_name = "payments"
+
+urlpatterns = [
+ path("checkout/", views.checkout, name="checkout"),
+ path("cart/", views.cart_detail, name="cart_detail"),
+ path("success/", SuccessView.as_view(), name="success"),
+ path("cancel/", CancelView.as_view(), name="cancel"),
+ path(
+ "create-checkout-session/<int:pk>/",
+ views.CreateCheckoutSessionView.as_view(),
+ name="create-checkout-session",
+ ),
+ path("add-to-card/<int:resource_id>", views.add_to_cart, name="add_to_cart"),
+ path("landing/", views.ProductLandingPageView.as_view(), name="landing"),
+ # path("webhook/", views.webhook, name="webhook"),
+]
diff --git a/alphabetlearning/payments/views.py b/alphabetlearning/payments/views.py
new file mode 100644
index 0000000..8337177
--- /dev/null
+++ b/alphabetlearning/payments/views.py
@@ -0,0 +1,96 @@
+import stripe
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.shortcuts import redirect
+from django.shortcuts import render
+from django.views import View
+from django.views.generic import TemplateView
+
+from alphabetlearning.resources.models import Resource
+from alphabetlearning.users.models import User
+
+from .models import CartItem
+from .models import Price
+from .models import Product
+from .models import ShoppingCart
+
+stripe.api_key = settings.STRIPE_SECRET_KEY
+
+
+class CreateCheckoutSessionView(View):
+ def post(self, request, *args, **kwargs):
+ price = Price.objects.get(id=self.kwargs["pk"])
+ domain = "http://localhost:8000"
+ checkout_session = stripe.checkout.Session.create(
+ payment_method_types=["card"],
+ line_items=[
+ {
+ "price": price.stripe_price_id,
+ "quantity": 1,
+ },
+ ],
+ mode="payment",
+ success_url=domain + "payments/success/",
+ cancel_url=domain + "payments/cancel/",
+ )
+ return redirect(checkout_session.url, code=303)
+
+
+class SuccessView(TemplateView):
+ template_name = "payments/success.html"
+
+
+class CancelView(TemplateView):
+ template_name = "payments/cancel.html"
+
+
+class ProductLandingPageView(TemplateView):
+ template_name = "payments/landingpage.html"
+
+ def get_context_data(self, **kwargs):
+ product = Product.objects.get(name="Worksheet 1")
+ prices = Price.objects.filter(product=product)
+ context = super(ProductLandingPageView, self).get_context_data(**kwargs)
+ context.update({"product": product, "prices": prices})
+ return context
+
+
+@login_required
+def add_to_cart(request, resource_id):
+ resource = get_object_or_404(Resource, id=resource_id)
+ cart, created = ShoppingCart.objects.get_or_create(user=request.user)
+ cart_item, created = CartItem.objects.get_or_create(cart=cart, resource=resource)
+ # cart_item.quantity += 1
+ cart_item.save()
+ return render(request, "payments/cart_detail.html", {"cart": cart})
+
+
+@login_required
+def cart_detail(request):
+ cart, created = ShoppingCart.objects.get_or_create(user=request.user)
+ return render(request, "payments/cart_detail.html", {"cart": cart})
+
+
+# def cart_detail(request):
+# cart, created = ShoppingCart.objects.get_or_create(user=request.user)
+# return render(request, "payments/cart_detail.html", {"cart": cart})
+
+
+@login_required
+def checkout(request):
+ cart = ShoppingCart.objects.get(user=request.user)
+ total = sum(item.get_total_price() for item in cart.items.all())
+
+ if request.method == "POST":
+ # Create Stripe PaymentIntent
+ intent = stripe.PaymentIntent.create(
+ amount=int(total * 100), # Stripe amount is in cents
+ currency="usd",
+ automatic_payment_methods={"enabled": True},
+ )
+
+ # Redirect to Stripe checkout or handle payment confirmation
+ return render(request, "cart/checkout_success.html")
+
+ return render(request, "cart/checkout.html", {"cart": cart, "total": total})