extensible-django-commerce 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. django_commerce/__init__.py +0 -0
  2. django_commerce/admin.py +1 -0
  3. django_commerce/apps.py +6 -0
  4. django_commerce/cart/__init__.py +0 -0
  5. django_commerce/cart/admin.py +1 -0
  6. django_commerce/cart/apps.py +6 -0
  7. django_commerce/cart/migrations/0001_initial.py +53 -0
  8. django_commerce/cart/migrations/0002_cart_active_cartitem__final_price_cartitem_active_and_more.py +37 -0
  9. django_commerce/cart/migrations/0003_remove_cart_user.py +17 -0
  10. django_commerce/cart/migrations/__init__.py +0 -0
  11. django_commerce/cart/models.py +86 -0
  12. django_commerce/cart/tests.py +1 -0
  13. django_commerce/cart/views.py +1 -0
  14. django_commerce/models.py +2 -0
  15. django_commerce/offsite_payment_gateway/__init__.py +0 -0
  16. django_commerce/offsite_payment_gateway/admin.py +1 -0
  17. django_commerce/offsite_payment_gateway/apps.py +11 -0
  18. django_commerce/offsite_payment_gateway/migrations/0001_initial.py +41 -0
  19. django_commerce/offsite_payment_gateway/migrations/__init__.py +0 -0
  20. django_commerce/offsite_payment_gateway/models.py +7 -0
  21. django_commerce/offsite_payment_gateway/plugins.py +35 -0
  22. django_commerce/offsite_payment_gateway/tests.py +1 -0
  23. django_commerce/offsite_payment_gateway/urls.py +7 -0
  24. django_commerce/offsite_payment_gateway/views.py +33 -0
  25. django_commerce/order/__init__.py +0 -0
  26. django_commerce/order/admin.py +1 -0
  27. django_commerce/order/apps.py +6 -0
  28. django_commerce/order/migrations/0001_initial.py +47 -0
  29. django_commerce/order/migrations/0002_remove_order_cart_items_order_cart_and_more.py +50 -0
  30. django_commerce/order/migrations/__init__.py +0 -0
  31. django_commerce/order/models.py +84 -0
  32. django_commerce/order/signals.py +5 -0
  33. django_commerce/order/tests.py +1 -0
  34. django_commerce/order/views.py +1 -0
  35. django_commerce/payment/__init__.py +0 -0
  36. django_commerce/payment/admin.py +1 -0
  37. django_commerce/payment/apps.py +11 -0
  38. django_commerce/payment/models.py +1 -0
  39. django_commerce/payment/plugins.py +11 -0
  40. django_commerce/payment/tests.py +1 -0
  41. django_commerce/payment/views.py +1 -0
  42. django_commerce/product/__init__.py +0 -0
  43. django_commerce/product/admin.py +1 -0
  44. django_commerce/product/apps.py +6 -0
  45. django_commerce/product/migrations/0001_initial.py +21 -0
  46. django_commerce/product/migrations/__init__.py +0 -0
  47. django_commerce/product/models.py +7 -0
  48. django_commerce/product/tests.py +1 -0
  49. django_commerce/product/views.py +1 -0
  50. django_commerce/tests.py +1 -0
  51. django_commerce/transaction/__init__.py +0 -0
  52. django_commerce/transaction/admin.py +1 -0
  53. django_commerce/transaction/apps.py +6 -0
  54. django_commerce/transaction/migrations/0001_initial.py +43 -0
  55. django_commerce/transaction/migrations/0002_remove_transaction_plugin_remove_transaction_result_and_more.py +30 -0
  56. django_commerce/transaction/migrations/__init__.py +0 -0
  57. django_commerce/transaction/models.py +19 -0
  58. django_commerce/transaction/tests.py +1 -0
  59. django_commerce/transaction/views.py +1 -0
  60. django_commerce/views.py +1 -0
  61. extensible_django_commerce-1.0.0.dist-info/METADATA +276 -0
  62. extensible_django_commerce-1.0.0.dist-info/RECORD +64 -0
  63. extensible_django_commerce-1.0.0.dist-info/WHEEL +5 -0
  64. extensible_django_commerce-1.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoCommerceConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_commerce'
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CartConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_commerce.cart'
@@ -0,0 +1,53 @@
1
+ # Generated by Django 6.0.4 on 2026-05-09 12:36
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("product", "0001_initial"),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="CartItem",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
24
+ ),
25
+ ),
26
+ ("quantity", models.PositiveIntegerField()),
27
+ (
28
+ "product",
29
+ models.ForeignKey(
30
+ on_delete=django.db.models.deletion.CASCADE, to="product.baseproduct"
31
+ ),
32
+ ),
33
+ ],
34
+ ),
35
+ migrations.CreateModel(
36
+ name="Cart",
37
+ fields=[
38
+ (
39
+ "id",
40
+ models.BigAutoField(
41
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
42
+ ),
43
+ ),
44
+ (
45
+ "user",
46
+ models.OneToOneField(
47
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
48
+ ),
49
+ ),
50
+ ("cart_items", models.ManyToManyField(to="cart.cartitem")),
51
+ ],
52
+ ),
53
+ ]
@@ -0,0 +1,37 @@
1
+ # Generated by Django 6.0.4 on 2026-06-23 12:06
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("cart", "0001_initial"),
11
+ ("product", "0001_initial"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="cart",
17
+ name="active",
18
+ field=models.BooleanField(default=True),
19
+ ),
20
+ migrations.AddField(
21
+ model_name="cartitem",
22
+ name="_final_price",
23
+ field=models.DecimalField(decimal_places=2, max_digits=20, null=True),
24
+ ),
25
+ migrations.AddField(
26
+ model_name="cartitem",
27
+ name="active",
28
+ field=models.BooleanField(default=True),
29
+ ),
30
+ migrations.AlterField(
31
+ model_name="cartitem",
32
+ name="product",
33
+ field=models.ForeignKey(
34
+ on_delete=django.db.models.deletion.PROTECT, to="product.baseproduct"
35
+ ),
36
+ ),
37
+ ]
@@ -0,0 +1,17 @@
1
+ # Generated by Django 6.0.4 on 2026-06-29 14:16
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("cart", "0002_cart_active_cartitem__final_price_cartitem_active_and_more"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name="cart",
15
+ name="user",
16
+ ),
17
+ ]
File without changes
@@ -0,0 +1,86 @@
1
+ from decimal import Decimal
2
+
3
+ from django.conf import settings
4
+ from django.db import models
5
+
6
+
7
+ class CartItem(models.Model):
8
+ product = models.ForeignKey('product.BaseProduct', on_delete=models.PROTECT)
9
+ quantity = models.PositiveIntegerField()
10
+ _final_price = models.DecimalField(max_digits=20, decimal_places=2,null=True)
11
+ active = models.BooleanField(default=True)
12
+
13
+ def __str__(self):
14
+ return f"{self.product.name} - {self.quantity}"
15
+
16
+
17
+ @property
18
+ def final_price(self):
19
+ return self._final_price
20
+
21
+ @final_price.setter
22
+ def final_price(self, value):
23
+ self._final_price = value
24
+
25
+ @property
26
+ def product_price(self):
27
+ return self.product.price if self.active else self.final_price or Decimal(0)
28
+
29
+ @property
30
+ def total(self):
31
+ return self.quantity * self.product_price
32
+
33
+
34
+ def increase_quantity(self):
35
+ self.quantity += 1
36
+
37
+ def decrease_quantity(self):
38
+ self.quantity -= 1
39
+
40
+ def payment_finished(self):
41
+ self.active = False
42
+ self.final_price = self.product.price
43
+ self.save()
44
+
45
+
46
+ class Cart(models.Model):
47
+ from django_commerce.product.models import BaseProduct
48
+
49
+ cart_items = models.ManyToManyField(CartItem)
50
+ active = models.BooleanField(default=True) # order is not paid
51
+
52
+ def add_to_cart(self, product: BaseProduct):
53
+ quantity_increased = False
54
+ for cart_item in self.cart_items.select_related('product').all():
55
+ if cart_item.product == product:
56
+ cart_item.increase_quantity()
57
+ cart_item.save()
58
+ quantity_increased = True
59
+ break
60
+ if not quantity_increased:
61
+ cart_item = CartItem.objects.create(product=product, quantity=1)
62
+ self.cart_items.add(cart_item)
63
+
64
+ def remove_from_cart(self, product: BaseProduct) -> bool:
65
+ for cart_item in self.cart_items.all():
66
+ if cart_item.product == product:
67
+ if cart_item.quantity == 1:
68
+ self.cart_items.remove(cart_item)
69
+ else:
70
+ cart_item.decrease_quantity()
71
+ cart_item.save()
72
+ return True
73
+ return False
74
+
75
+ @property
76
+ def total(self):
77
+ total = Decimal(0)
78
+ for cart_item in self.cart_items.all():
79
+ total += cart_item.total
80
+ return total
81
+
82
+ def close_cart(self):
83
+ for cart_item in self.cart_items.all():
84
+ cart_item.payment_finished()
85
+ self.active = False
86
+ self.save()
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1 @@
1
+ # Create your views here.
@@ -0,0 +1,2 @@
1
+
2
+
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,11 @@
1
+ from django.apps import AppConfig
2
+ from django_plugin_system import register_plugin_type
3
+
4
+
5
+ class OffsitePaymentGatewayConfig(AppConfig):
6
+ default_auto_field = 'django.db.models.BigAutoField'
7
+ name = 'django_commerce.offsite_payment_gateway'
8
+
9
+ def ready(self):
10
+ from .plugins import OffsitePayment
11
+ register_plugin_type({"interface":OffsitePayment})
@@ -0,0 +1,41 @@
1
+ # Generated by Django 6.0.4 on 2026-06-23 12:08
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("django_plugin_system", "0001_initial"),
13
+ ("order", "0002_remove_order_cart_items_order_cart_and_more"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="InProgressPayment",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
24
+ ),
25
+ ),
26
+ (
27
+ "order",
28
+ models.ForeignKey(
29
+ on_delete=django.db.models.deletion.PROTECT, to="order.order"
30
+ ),
31
+ ),
32
+ (
33
+ "plugin_instance",
34
+ models.ForeignKey(
35
+ on_delete=django.db.models.deletion.PROTECT,
36
+ to="django_plugin_system.plugininstance",
37
+ ),
38
+ ),
39
+ ],
40
+ ),
41
+ ]
@@ -0,0 +1,7 @@
1
+ from django.db import models
2
+ from django_plugin_system.models import PluginInstance
3
+
4
+
5
+ class InProgressPayment(models.Model):
6
+ plugin_instance = models.ForeignKey(PluginInstance, on_delete=models.PROTECT)
7
+ order = models.ForeignKey('order.Order', on_delete=models.PROTECT)
@@ -0,0 +1,35 @@
1
+ from typing import Tuple, Dict
2
+
3
+ from django.http import HttpRequest, HttpResponseRedirect
4
+ from django.urls import reverse
5
+ from django_plugin_system import required_plugin_item_method
6
+
7
+ from ..payment.plugins import PaymentPlugin
8
+
9
+ BASE_URL = 'http://127.0.0.1:8000'
10
+
11
+ class OffsitePayment(PaymentPlugin):
12
+ name = 'Offsite Payment'
13
+
14
+ def create_payment(self, order):
15
+ from .models import InProgressPayment
16
+ in_progress_payment = InProgressPayment.objects.create(
17
+ order=order,
18
+ plugin_instance=self.plugin_item.plugin_instance
19
+ )
20
+ callback_url = BASE_URL + HttpResponseRedirect(reverse('payment_callback', args=[in_progress_payment.pk])).url
21
+ return self.plugin_item.get_payment_gateway_url(order, callback_url)
22
+
23
+ @required_plugin_item_method
24
+ def get_payment_gateway_url(self, order, callback_url: str) -> Tuple[bool,str]:
25
+ """
26
+ :param order:
27
+ :param callback_url:
28
+ :return: str
29
+ The return value should be the gateway url as string
30
+ """
31
+ pass
32
+
33
+ @required_plugin_item_method
34
+ def verify_payment(self, order, callback_request: HttpRequest | None = None) -> Tuple[bool, Dict]:
35
+ pass
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1,7 @@
1
+ from django.urls import path
2
+
3
+ from .views import callback_view
4
+
5
+ urlpatterns = [
6
+ path('payment-callback/<int:payment_id>', callback_view, name='payment_callback'),
7
+ ]
@@ -0,0 +1,33 @@
1
+ from django.conf import settings
2
+ from django.http import HttpRequest
3
+ from django.http import HttpResponse
4
+ from django.shortcuts import redirect
5
+
6
+ from .models import InProgressPayment
7
+ from ..transaction.models import Transaction
8
+
9
+
10
+ def callback_view(request: HttpRequest, payment_id):
11
+ system_error = getattr(settings, "OFFSITE_PAYMENT_SYSTEM_ERROR", None)
12
+ payment_not_verified = getattr(settings, "OFFSITE_PAYMENT_NOT_VERIFIED", None)
13
+ payment_success = getattr(settings, "OFFSITE_PAYMENT_SUCCESS", None)
14
+ try:
15
+ processing_payment = InProgressPayment.objects.select_related('order', 'plugin_instance').get(pk=payment_id)
16
+ order = processing_payment.order
17
+ plugin_instance = processing_payment.plugin_instance
18
+ processing_payment.delete()
19
+ transaction = Transaction.objects.create(order=processing_payment.order)
20
+ plugin_implementation = plugin_instance.load_implementation()
21
+ if not plugin_implementation:
22
+ transaction.failed("Offsite payment gateway instance not found!")
23
+ return redirect(system_error) if system_error else "System Error: undefined payment gateway"
24
+ verified, details = plugin_implementation.verify_payment(order, request)
25
+ print(details)
26
+ if verified:
27
+ transaction.succeeded(details)
28
+ print('verified')
29
+ return redirect(payment_success) if payment_success else HttpResponse(status=200, content=details)
30
+ transaction.failed(details)
31
+ return redirect(payment_not_verified) if payment_not_verified else HttpResponse(status=403, content=details)
32
+ except InProgressPayment.DoesNotExist:
33
+ return redirect(system_error) if system_error else HttpResponse(status=404, content='payment not listed!')
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class OrderConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_commerce.order'
@@ -0,0 +1,47 @@
1
+ # Generated by Django 6.0.4 on 2026-05-09 12:36
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("cart", "0001_initial"),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="Order",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
24
+ ),
25
+ ),
26
+ (
27
+ "status",
28
+ models.IntegerField(
29
+ choices=[
30
+ (2, "Pending"),
31
+ (3, "Processing"),
32
+ (4, "Completed"),
33
+ (5, "Canceled"),
34
+ ],
35
+ default=2,
36
+ ),
37
+ ),
38
+ ("cart_items", models.ManyToManyField(to="cart.cartitem")),
39
+ (
40
+ "user",
41
+ models.ForeignKey(
42
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
43
+ ),
44
+ ),
45
+ ],
46
+ ),
47
+ ]
@@ -0,0 +1,50 @@
1
+ # Generated by Django 6.0.4 on 2026-06-23 12:06
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("cart", "0002_cart_active_cartitem__final_price_cartitem_active_and_more"),
12
+ ("order", "0001_initial"),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.RemoveField(
18
+ model_name="order",
19
+ name="cart_items",
20
+ ),
21
+ migrations.AddField(
22
+ model_name="order",
23
+ name="cart",
24
+ field=models.ForeignKey(
25
+ null=True, on_delete=django.db.models.deletion.PROTECT, to="cart.cart"
26
+ ),
27
+ ),
28
+ migrations.AlterField(
29
+ model_name="order",
30
+ name="status",
31
+ field=models.IntegerField(
32
+ choices=[
33
+ (0, "Draft"),
34
+ (1, "Processing"),
35
+ (2, "Pending"),
36
+ (3, "Paid"),
37
+ (4, "Completed"),
38
+ (5, "Canceled"),
39
+ ],
40
+ default=0,
41
+ ),
42
+ ),
43
+ migrations.AlterField(
44
+ model_name="order",
45
+ name="user",
46
+ field=models.ForeignKey(
47
+ null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
48
+ ),
49
+ ),
50
+ ]
File without changes
@@ -0,0 +1,84 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+
4
+ from django.conf import settings
5
+ from django.contrib.auth.models import User
6
+ from django.db import models
7
+ from django_plugin_system import BasePluginItem
8
+ from django_plugin_system.models import PluginType
9
+
10
+ from .signals import order_payment_completed, order_payment_failed, order_canceled
11
+ from ..cart.models import Cart
12
+ from ..payment.plugins import PaymentPlugin
13
+
14
+
15
+ class OrderState(models.IntegerChoices):
16
+ DRAFT = 0
17
+ PROCESSING = 1
18
+ PENDING = 2
19
+ PAID = 3
20
+ COMPLETED = 4
21
+ CANCELED = 5
22
+
23
+ IN_PROGRESS_ORDERS = [OrderState.DRAFT, OrderState.PROCESSING, OrderState.PENDING]
24
+
25
+ @dataclass
26
+ class EmptyOrderException(Exception):
27
+ message: str = "The empty order can not be paid."
28
+
29
+ @dataclass
30
+ class NoPaymentPluginException(Exception):
31
+ message: str = "No payment plugin is available."
32
+
33
+
34
+ class Order(models.Model):
35
+ User = settings.AUTH_USER_MODEL
36
+
37
+ cart = models.ForeignKey(Cart, on_delete=models.PROTECT, null=True)
38
+ user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
39
+ status = models.IntegerField(choices=OrderState, default=OrderState.DRAFT)
40
+
41
+ @property
42
+ def is_empty(self):
43
+ return self.cart.cart_items.count() == 0
44
+
45
+ @property
46
+ def total_price(self):
47
+ return self.cart.total
48
+
49
+ def do_payment(self, payment_plugin: BasePluginItem | None = None):
50
+ if self.is_empty:
51
+ raise EmptyOrderException()
52
+ if not payment_plugin:
53
+ payment_type = PluginType.get_plugin_type_by_class(PaymentPlugin)
54
+ payment_plugin_instance = payment_type.get_single_plugin()
55
+ if not payment_plugin_instance:
56
+ raise NoPaymentPluginException()
57
+ payment_plugin = payment_plugin_instance.load_implementation()
58
+ return payment_plugin.create_payment(self)
59
+
60
+
61
+ def order_paid_successfully(self, transaction_info: Dict):
62
+ self.cart.close_cart()
63
+ self.status = OrderState.PAID
64
+ self.save()
65
+ order_payment_completed.send(sender=self.__class__, order=self, info=transaction_info)
66
+
67
+ def order_payment_failed(self, transaction_info):
68
+ self.status = OrderState.PENDING
69
+ self.save()
70
+ order_payment_failed.send(sender=self.__class__, order=self, info=transaction_info)
71
+
72
+ def cancel_order(self):
73
+ self.status = OrderState.CANCELED
74
+ self.cart.close_cart()
75
+ self.save()
76
+ order_canceled.send(sender=self.__class__, order=self)
77
+
78
+ @staticmethod
79
+ def get_user_current_order(user: User):
80
+ try:
81
+ return Order.objects.get(user=user, status__in=IN_PROGRESS_ORDERS)
82
+ except Order.DoesNotExist:
83
+ cart = Cart.objects.create()
84
+ return Order.objects.create(user=user, cart=cart)
@@ -0,0 +1,5 @@
1
+ from django.dispatch import Signal
2
+
3
+ order_payment_completed = Signal()
4
+ order_payment_failed = Signal()
5
+ order_canceled = Signal()
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1 @@
1
+ # Create your views here.
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,11 @@
1
+ from django.apps import AppConfig
2
+ from django_plugin_system import register_plugin_type
3
+
4
+
5
+ class PaymentConfig(AppConfig):
6
+ default_auto_field = 'django.db.models.BigAutoField'
7
+ name = 'django_commerce.payment'
8
+
9
+ def ready(self):
10
+ from .plugins import PaymentPlugin
11
+ register_plugin_type({"interface": PaymentPlugin})
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,11 @@
1
+ from abc import abstractmethod
2
+
3
+ from django_plugin_system import BasePluginType
4
+
5
+
6
+ class PaymentPlugin(BasePluginType):
7
+ name = 'payment'
8
+
9
+ @abstractmethod
10
+ def create_payment(self, order):
11
+ pass
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1 @@
1
+ # Create your views here.
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ProductConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_commerce.product'
@@ -0,0 +1,21 @@
1
+ # Generated by Django 6.0 on 2026-01-05 11:38
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ initial = True
8
+
9
+ dependencies = [
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='BaseProduct',
15
+ fields=[
16
+ ('squid', models.CharField(max_length=30, primary_key=True, serialize=False)),
17
+ ('name', models.CharField(max_length=30)),
18
+ ('price', models.DecimalField(decimal_places=2, max_digits=10)),
19
+ ],
20
+ ),
21
+ ]
File without changes
@@ -0,0 +1,7 @@
1
+ from django.db import models
2
+
3
+
4
+ class BaseProduct(models.Model):
5
+ squid = models.CharField(max_length=30, primary_key=True)
6
+ name = models.CharField(max_length=30)
7
+ price = models.DecimalField(max_digits=20, decimal_places=2)
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1 @@
1
+ # Create your views here.
@@ -0,0 +1 @@
1
+ # Create your tests here.
File without changes
@@ -0,0 +1 @@
1
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class TransactionConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_commerce.transaction'
@@ -0,0 +1,43 @@
1
+ # Generated by Django 6.0.4 on 2026-05-09 12:37
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ("django_plugin_system", "__first__"),
12
+ ("order", "0001_initial"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="Transaction",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.BigAutoField(
22
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
23
+ ),
24
+ ),
25
+ ("tracing_code", models.CharField(max_length=100, null=True)),
26
+ ("result", models.TextField(null=True)),
27
+ ("success", models.BooleanField(default=False)),
28
+ (
29
+ "order",
30
+ models.ForeignKey(
31
+ null=True, on_delete=django.db.models.deletion.PROTECT, to="order.order"
32
+ ),
33
+ ),
34
+ (
35
+ "plugin",
36
+ models.ForeignKey(
37
+ on_delete=django.db.models.deletion.PROTECT,
38
+ to="django_plugin_system.pluginitem",
39
+ ),
40
+ ),
41
+ ],
42
+ ),
43
+ ]
@@ -0,0 +1,30 @@
1
+ # Generated by Django 6.0.4 on 2026-06-23 12:06
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("transaction", "0001_initial"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name="transaction",
15
+ name="plugin",
16
+ ),
17
+ migrations.RemoveField(
18
+ model_name="transaction",
19
+ name="result",
20
+ ),
21
+ migrations.RemoveField(
22
+ model_name="transaction",
23
+ name="tracing_code",
24
+ ),
25
+ migrations.AddField(
26
+ model_name="transaction",
27
+ name="info",
28
+ field=models.JSONField(null=True),
29
+ ),
30
+ ]
File without changes
@@ -0,0 +1,19 @@
1
+ from typing import Dict
2
+ from django.db import models
3
+
4
+
5
+ class Transaction(models.Model):
6
+ order = models.ForeignKey('order.Order', on_delete=models.PROTECT, null=True)
7
+ info = models.JSONField(null=True)
8
+ success = models.BooleanField(default=False)
9
+
10
+ def failed(self, reason: str | Dict):
11
+ self.info = reason if isinstance(reason, dict) else {"error": reason}
12
+ self.save()
13
+ self.order.order_payment_failed(self.info)
14
+
15
+ def succeeded(self, info: dict):
16
+ self.success = True
17
+ self.info = info
18
+ self.save()
19
+ self.order.order_paid_successfully(self.info)
@@ -0,0 +1 @@
1
+ # Create your tests here.
@@ -0,0 +1 @@
1
+ # Create your views here.
@@ -0,0 +1 @@
1
+ # Create your views here.
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: extensible-django-commerce
3
+ Version: 1.0.0
4
+ Summary: Basic yet highly extensible requirements for commerce in django.
5
+ Author-email: Alireza Tabatabaeian <alireza.tabatabaeian@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Alireza-Tabatabaeian/django-commerce
7
+ Project-URL: Issues, https://github.com/Alireza-Tabatabaeian/django-commerce/issues
8
+ Keywords: django,commerce,cart,offsite payment gateway,extensibility
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.0
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: Django>=4.2
26
+ Requires-Dist: django-plugin-system>=2.0.8
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: pytest-django>=4.8.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.8; extra == "dev"
31
+ Requires-Dist: django-stubs>=5.0.0; extra == "dev"
32
+ Requires-Dist: black>=24.0.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
34
+
35
+ # Django Commerce
36
+
37
+ A lightweight and extensible commerce foundation for Django projects.
38
+
39
+ `django-commerce` provides reusable commerce primitives such as products, carts, orders, transactions, and pluggable offsite payment gateways — designed to integrate cleanly with modular Django architectures and plugin-based systems.
40
+
41
+ This package is built on top of [django-plugin-system](https://github.com/Alireza-Tabatabaeian/django-plugin-system).
42
+
43
+ CAUTION: The package has been changed signifactly and README does not guide well right now. Will be modified in near future.
44
+
45
+ ---
46
+
47
+ # Features
48
+
49
+ * Base product abstraction
50
+ * Shopping cart system
51
+ * Order management
52
+ * Transaction tracking
53
+ * Extensible offsite payment gateway architecture
54
+ * Plugin-based payment providers
55
+ * Designed for reusable Django applications
56
+
57
+ ---
58
+
59
+ # Installation
60
+
61
+ Install using pip:
62
+
63
+ ```bash
64
+ pip install django-commerce
65
+ ```
66
+
67
+ Add applications to your `INSTALLED_APPS`:
68
+
69
+ ```python
70
+ INSTALLED_APPS = [
71
+ ...
72
+
73
+ 'django_plugin_system',
74
+
75
+ 'django_commerce.product',
76
+ 'django_commerce.cart',
77
+ 'django_commerce.order',
78
+ 'django_commerce.transaction',
79
+ 'django_commerce.offsite_payment_gateway',
80
+ ]
81
+ ```
82
+
83
+ Run migrations:
84
+
85
+ ```bash
86
+ python manage.py migrate
87
+ ```
88
+
89
+ ---
90
+
91
+ # Package Structure
92
+
93
+ ```text
94
+ django_commerce/
95
+ ├── product/
96
+ ├── cart/
97
+ ├── order/
98
+ ├── transaction/
99
+ └── offsite_payment_gateway/
100
+ ```
101
+
102
+ ---
103
+
104
+ # Base Product
105
+
106
+ The package provides a reusable `BaseProduct` model abstraction that can be extended in your own project.
107
+
108
+ Example:
109
+
110
+ ```python
111
+ from django.db import models
112
+ from django_commerce.product.models import BaseProduct
113
+
114
+
115
+ class Product(BaseProduct):
116
+ description = models.TextField()
117
+ ```
118
+
119
+ ---
120
+
121
+ # Offsite Payment Gateway
122
+
123
+ The package includes an abstract offsite payment gateway system that allows developers to integrate external payment providers such as:
124
+
125
+ * Zarinpal
126
+ * Stripe Checkout
127
+ * PayPal
128
+ * Mollie
129
+ * Any redirect-based payment provider
130
+
131
+ To create a custom gateway, extend `AbstractOffsitePaymentGateway`.
132
+
133
+ ---
134
+
135
+ # Creating a Payment Gateway
136
+
137
+ Example:
138
+
139
+ ```python
140
+ from django.http import HttpRequest
141
+
142
+ from django_commerce.offsite_payment_gateway.models import (
143
+ AbstractOffsitePaymentGateway
144
+ )
145
+
146
+
147
+ class ZarinpalGateway(AbstractOffsitePaymentGateway):
148
+ name = 'zarinpal'
149
+ _gateway_base_url = 'https://sandbox.zarinpal.com'
150
+
151
+ def get_payment_gateway(self, order):
152
+ transaction = self.create_transaction(order)
153
+
154
+ callback = self.callback_url(transaction)
155
+
156
+ return f'{self._gateway_base_url}/start/{transaction.id}?callback={callback}'
157
+
158
+ def verify_payment(self, transaction, callback_request: HttpRequest) -> bool:
159
+ authority = callback_request.GET.get('Authority')
160
+
161
+ if not authority:
162
+ return False
163
+
164
+ # Verify payment using provider API
165
+ # ...
166
+
167
+ transaction.is_verified = True
168
+ transaction.save()
169
+
170
+ return True
171
+ ```
172
+
173
+ ---
174
+
175
+ # Registering Gateway Plugins
176
+
177
+ Gateways are designed to work with the plugin system.
178
+
179
+ Example registration:
180
+
181
+ ```python
182
+ from django_plugin_system import register_plugin
183
+
184
+ from .gateway import ZarinpalGateway
185
+
186
+
187
+ register_plugin(
188
+ manager='django_commerce.offsite_payment_gateway',
189
+ title='Zarinpal Gateway',
190
+ plugin=ZarinpalGateway,
191
+ )
192
+ ```
193
+
194
+ ---
195
+
196
+ # Payment Callback View
197
+
198
+ The package provides a callback endpoint handler for payment verification.
199
+
200
+ Example URL configuration:
201
+
202
+ ```python
203
+ from django.urls import path
204
+
205
+ from django_commerce.offsite_payment_gateway.views import callback_view
206
+
207
+
208
+ urlpatterns = [
209
+ path(
210
+ 'payment-callback/<int:transaction_id>',
211
+ callback_view,
212
+ name='payment_callback'
213
+ ),
214
+ ]
215
+ ```
216
+
217
+ After the payment provider redirects the user back to your application, the callback view:
218
+
219
+ 1. Loads the related transaction
220
+ 2. Resolves the payment gateway plugin
221
+ 3. Calls `verify_payment`
222
+ 4. Verifies and finalizes the transaction
223
+
224
+ ---
225
+
226
+ # Transactions
227
+
228
+ Each payment attempt creates a `Transaction` object associated with:
229
+
230
+ * Order
231
+ * Payment plugin
232
+ * Verification state
233
+
234
+ This makes payment tracking and auditing easier across multiple providers.
235
+
236
+ ---
237
+
238
+ # Design Goals
239
+
240
+ This package focuses on:
241
+
242
+ * Reusability
243
+ * Extensibility
244
+ * Minimal assumptions
245
+ * Plugin-oriented architecture
246
+ * Separation of concerns
247
+
248
+ It is intended to act as a commerce foundation rather than a complete e-commerce platform.
249
+
250
+ ---
251
+
252
+ # Development
253
+
254
+ Clone the repository:
255
+
256
+ ```bash
257
+ git clone <repository-url>
258
+ ```
259
+
260
+ Install in editable mode:
261
+
262
+ ```bash
263
+ pip install -e .
264
+ ```
265
+
266
+ Run migrations from the development project:
267
+
268
+ ```bash
269
+ python manage.py migrate
270
+ ```
271
+
272
+ ---
273
+
274
+ # License
275
+
276
+ MIT License
@@ -0,0 +1,64 @@
1
+ django_commerce/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ django_commerce/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
3
+ django_commerce/apps.py,sha256=XvsQ2GOZ_MEMfNABRVnlCB5Ppob3QjPrAwcqikSri40,167
4
+ django_commerce/models.py,sha256=26UWatnbm6ZIwQMuu9NNzQ0IW1ACO4Oe9caModuTpWM,4
5
+ django_commerce/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
6
+ django_commerce/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
7
+ django_commerce/cart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ django_commerce/cart/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
9
+ django_commerce/cart/apps.py,sha256=IMwEVY-k_7sSmTeaNRBH9xe87lm6DbiuAn0cCHEnK8Y,162
10
+ django_commerce/cart/models.py,sha256=udeUsZSshq9DabTkLgYLim13w6POULUXAhuxWbb17e4,2631
11
+ django_commerce/cart/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
12
+ django_commerce/cart/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
13
+ django_commerce/cart/migrations/0001_initial.py,sha256=A7k5pJHO9a3zfhSfNmOYhdgS5BxX0RxAEMFn3H90ePU,1673
14
+ django_commerce/cart/migrations/0002_cart_active_cartitem__final_price_cartitem_active_and_more.py,sha256=xaqDBmGEsBt292yJtaU8YhSdphSZebH9MJfQcniP5Es,1066
15
+ django_commerce/cart/migrations/0003_remove_cart_user.py,sha256=xUljWXDgzH5qoCrBYzY3IVfD2DzNnuiN4siG21O1KaM,378
16
+ django_commerce/cart/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ django_commerce/offsite_payment_gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ django_commerce/offsite_payment_gateway/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
19
+ django_commerce/offsite_payment_gateway/apps.py,sha256=rZACkIqhQsCMbduKHy94mE3-oZtgC7J0_iOVVnaaTRo,382
20
+ django_commerce/offsite_payment_gateway/models.py,sha256=SMNzNL3xTLtAWj__WMpDAaUm-xdKXwI4MOEOKk2JUMk,285
21
+ django_commerce/offsite_payment_gateway/plugins.py,sha256=sekOT8w1cWHrI-kLJWLst11IhWmHY5Rad4Z2qjoZS3A,1245
22
+ django_commerce/offsite_payment_gateway/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
23
+ django_commerce/offsite_payment_gateway/urls.py,sha256=HQLnfMXzppG92EL-gBKvICYqFLUic5h1ogJw0Snqrl0,176
24
+ django_commerce/offsite_payment_gateway/views.py,sha256=F-InU5jNGSc4UWi53tFo4bG0vjOMkS-KCghn7BkwxUc,1803
25
+ django_commerce/offsite_payment_gateway/migrations/0001_initial.py,sha256=ftRQyPIdrIHa62DF4Y4CXX_C3tTUzydsf14y1txrinA,1227
26
+ django_commerce/offsite_payment_gateway/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ django_commerce/order/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ django_commerce/order/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
29
+ django_commerce/order/apps.py,sha256=yyuB6wNl-xJryEMIjSwNcaYmaU34DQ0S0eXUUXNqH_g,164
30
+ django_commerce/order/models.py,sha256=oLMsEWtaS-OCkkv0er9sLsIgDG0Ie7kltuIF_w7POQg,2864
31
+ django_commerce/order/signals.py,sha256=QCijQSfLYqBDnT50ffUZc1pUnF9jQreSBd7zYMxe3BU,134
32
+ django_commerce/order/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
33
+ django_commerce/order/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
34
+ django_commerce/order/migrations/0001_initial.py,sha256=t-eptwLQ3iIYQVP9ZOqu5b8JWar06kVwi4iuY2z8-Kg,1455
35
+ django_commerce/order/migrations/0002_remove_order_cart_items_order_cart_and_more.py,sha256=GqZdHE6k4YGrntKxIHTu8cwftPc0T3P8Eqe9A1sR5bU,1525
36
+ django_commerce/order/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ django_commerce/payment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ django_commerce/payment/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
39
+ django_commerce/payment/apps.py,sha256=bfKVh3FXrReVzEdTjgeVnjRYevvUXDebK6yN3hqwjBI,351
40
+ django_commerce/payment/models.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
41
+ django_commerce/payment/plugins.py,sha256=w-7bmkzEf0i4Et3hhxsaO3ndgGMS43EAY3p2AsCiBWo,222
42
+ django_commerce/payment/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
43
+ django_commerce/payment/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
44
+ django_commerce/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ django_commerce/product/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
46
+ django_commerce/product/apps.py,sha256=ewryYz-X52DG8eK3u1TUJj4wR8JxUIdnBYxhk1qN6W0,168
47
+ django_commerce/product/models.py,sha256=qmtFw3PZF88dMCRmO_Eh-YSOfqp8osCMQ27VMyTr-A0,241
48
+ django_commerce/product/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
49
+ django_commerce/product/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
50
+ django_commerce/product/migrations/0001_initial.py,sha256=CT82hdrg3EQ31pxfGcJNW5n7OGzXCjO7OtCzLlDqtOY,569
51
+ django_commerce/product/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ django_commerce/transaction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ django_commerce/transaction/admin.py,sha256=xZHn9b5u2_7bD6Q48FnN0WiRPjEbE9c9dIm4aIe1M34,30
54
+ django_commerce/transaction/apps.py,sha256=5LoPgmNyQWD6-YE3MwkKLGxDiafbbLglfVxeS8KVloA,176
55
+ django_commerce/transaction/models.py,sha256=PeztXfrNt25KnkoH3mInc4fx7qcoP9AsW4pu_MwV7nE,634
56
+ django_commerce/transaction/tests.py,sha256=p1bwJbR_nFqJjyfOygNEO5nXSrU3Ku0A3g2O8oZ74c0,27
57
+ django_commerce/transaction/views.py,sha256=gQmOOQQgt5SMeC_hOzhacKuRPDLbcVfNqURLhP0M1U4,27
58
+ django_commerce/transaction/migrations/0001_initial.py,sha256=M8TCDrNQMBMuCm1-Wz3ASBp900cI6MUXZVkexuuhzYk,1382
59
+ django_commerce/transaction/migrations/0002_remove_transaction_plugin_remove_transaction_result_and_more.py,sha256=1gDFNl8mSrekG1VUxagQtXCZ9Aw_L6DPz__-1jArNls,736
60
+ django_commerce/transaction/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
+ extensible_django_commerce-1.0.0.dist-info/METADATA,sha256=naPWReJ6hFeX3oZ_zUSVy8kX4-GvqOYEl8_qeq_PAnQ,6425
62
+ extensible_django_commerce-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
63
+ extensible_django_commerce-1.0.0.dist-info/top_level.txt,sha256=WO2yqDsJXl5hv5hRu4jwb8ZKtiRewlW-athKvDtKGfI,16
64
+ extensible_django_commerce-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ django_commerce