ob-dj-store 0.0.19__py3-none-any.whl → 0.0.23.2__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 (82) hide show
  1. ob_dj_store/apis/stores/filters.py +42 -19
  2. ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
  3. ob_dj_store/apis/stores/urls.py +6 -0
  4. ob_dj_store/apis/stores/views.py +140 -227
  5. ob_dj_store/apis/stripe/__init__.py +0 -0
  6. ob_dj_store/apis/stripe/serializers.py +185 -0
  7. ob_dj_store/apis/stripe/urls.py +25 -0
  8. ob_dj_store/apis/stripe/views.py +191 -0
  9. ob_dj_store/apis/tap/views.py +2 -6
  10. ob_dj_store/core/stores/admin.py +41 -38
  11. ob_dj_store/core/stores/admin_inlines.py +8 -13
  12. ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
  13. ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
  14. ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
  15. ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
  16. ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
  17. ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
  18. ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
  19. ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
  20. ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
  21. ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
  22. ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
  23. ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
  24. ob_dj_store/core/stores/gateway/tap/models.py +4 -13
  25. ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
  26. ob_dj_store/core/stores/managers.py +12 -4
  27. ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
  28. ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
  29. ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
  30. ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
  31. ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
  32. ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
  33. ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
  34. ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
  35. ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
  36. ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
  37. ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
  38. ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
  39. ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
  40. ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
  41. ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
  42. ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
  43. ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
  44. ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
  45. ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
  46. ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
  47. ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
  48. ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
  49. ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
  50. ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
  51. ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
  52. ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
  53. ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
  54. ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
  55. ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
  56. ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
  57. ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
  58. ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
  59. ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
  60. ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
  61. ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
  62. ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
  63. ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
  64. ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
  65. ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
  66. ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
  67. ob_dj_store/core/stores/models/__init__.py +9 -1
  68. ob_dj_store/core/stores/models/_address.py +1 -3
  69. ob_dj_store/core/stores/models/_cart.py +11 -5
  70. ob_dj_store/core/stores/models/_feedback.py +1 -3
  71. ob_dj_store/core/stores/models/_inventory.py +3 -2
  72. ob_dj_store/core/stores/models/_order.py +69 -20
  73. ob_dj_store/core/stores/models/_payment.py +28 -24
  74. ob_dj_store/core/stores/models/_product.py +31 -17
  75. ob_dj_store/core/stores/models/_store.py +9 -13
  76. ob_dj_store/core/stores/models/_wallet.py +34 -26
  77. ob_dj_store/core/stores/receivers.py +43 -27
  78. ob_dj_store/core/stores/utils.py +1 -2
  79. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
  80. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
  81. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
  82. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,35 @@
1
+ # Generated by Django 3.2.8 on 2024-12-22 19:07
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0105_store_is_open_after_midnight"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name="paymentmethod",
15
+ name="payment_provider",
16
+ field=models.CharField(
17
+ choices=[
18
+ ("cod", "cash on delivery"),
19
+ ("src_all", "TAP all payment methods"),
20
+ ("src_card", "Tap Credit Card"),
21
+ ("src_kw.knet", "Tap knet"),
22
+ ("paypal", "Paypal"),
23
+ ("stripe", "Stripe"),
24
+ ("wallet", "Wallet"),
25
+ ("gift", "Gift"),
26
+ ("src_apple_pay", "Apple Pay"),
27
+ ("google_pay", "Google Pay"),
28
+ ("src_sa.mada", "Mada"),
29
+ ("src_bh.benefit", "Benefit"),
30
+ ],
31
+ default="cod",
32
+ max_length=20,
33
+ ),
34
+ ),
35
+ ]
@@ -0,0 +1,29 @@
1
+ # Generated by Django 3.2.8 on 2025-04-25 17:59
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0106_alter_paymentmethod_payment_provider"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(model_name="store", name="is_open_after_midnight",),
14
+ migrations.AddField(
15
+ model_name="openinghours",
16
+ name="is_open_after_midnight",
17
+ field=models.BooleanField(default=False),
18
+ ),
19
+ migrations.AddField(
20
+ model_name="order",
21
+ name="offer_id",
22
+ field=models.IntegerField(blank=True, default=None, null=True),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="order",
26
+ name="offer_redeemed",
27
+ field=models.BooleanField(default=False),
28
+ ),
29
+ ]
@@ -0,0 +1,35 @@
1
+ # Generated by Django 3.2.8 on 2025-04-29 20:27
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0107_auto_20250425_2059"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name="paymentmethod",
15
+ name="payment_provider",
16
+ field=models.CharField(
17
+ choices=[
18
+ ("cod", "cash on delivery"),
19
+ ("src_all", "TAP all payment methods"),
20
+ ("src_card", "Tap Credit Card"),
21
+ ("src_kw.knet", "Tap knet"),
22
+ ("paypal", "Paypal"),
23
+ ("stripe", "Stripe"),
24
+ ("wallet", "Wallet"),
25
+ ("gift", "Gift"),
26
+ ("apple_pay", "Apple Pay"),
27
+ ("google_pay", "Google Pay"),
28
+ ("src_sa.mada", "Mada"),
29
+ ("src_bh.benefit", "Benefit"),
30
+ ],
31
+ default="cod",
32
+ max_length=20,
33
+ ),
34
+ ),
35
+ ]
@@ -0,0 +1,27 @@
1
+ # Generated by Django 3.2.8 on 2025-07-31 16:12
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0108_alter_paymentmethod_payment_provider"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="wallettransaction",
15
+ name="cashback_type",
16
+ field=models.CharField(
17
+ blank=True,
18
+ choices=[
19
+ ("BY_ORDER", "By Order"),
20
+ ("BY_OFFER", "By Offer"),
21
+ ("BY_STREAK", "By Streak"),
22
+ ],
23
+ max_length=100,
24
+ null=True,
25
+ ),
26
+ ),
27
+ ]
@@ -0,0 +1,26 @@
1
+ # Generated by Django 3.2.8 on 2025-09-23 14:14
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0109_wallettransaction_cashback_type"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name="productvariant",
15
+ options={
16
+ "ordering": ("order_value",),
17
+ "verbose_name": "Product Variation",
18
+ "verbose_name_plural": "Product Variations",
19
+ },
20
+ ),
21
+ migrations.AddField(
22
+ model_name="productvariant",
23
+ name="order_value",
24
+ field=models.PositiveSmallIntegerField(default=1, verbose_name="ordering"),
25
+ ),
26
+ ]
@@ -0,0 +1,35 @@
1
+ # Generated by Django 3.2.8 on 2025-10-23 14:00
2
+
3
+ import timezone_field.fields
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("stores", "0110_auto_20250923_1714"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="store",
16
+ name="has_daylight_saving_time",
17
+ field=models.BooleanField(default=False),
18
+ ),
19
+ migrations.AddField(
20
+ model_name="store",
21
+ name="timezone",
22
+ field=timezone_field.fields.TimeZoneField(default="Asia/Kuwait"),
23
+ ),
24
+ migrations.AlterField(
25
+ model_name="tax",
26
+ name="value",
27
+ field=models.DecimalField(
28
+ blank=True,
29
+ decimal_places=5,
30
+ help_text="Value for the given Payment -> 0.0625",
31
+ max_digits=7,
32
+ null=True,
33
+ ),
34
+ ),
35
+ ]
@@ -0,0 +1,98 @@
1
+ # Generated by Django 3.2.8 on 2025-10-27 14:39
2
+
3
+ import django.db.models.deletion
4
+ import django_countries.fields
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("stores", "0111_auto_20251023_1700"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name="Tip",
17
+ fields=[
18
+ (
19
+ "id",
20
+ models.AutoField(
21
+ auto_created=True,
22
+ primary_key=True,
23
+ serialize=False,
24
+ verbose_name="ID",
25
+ ),
26
+ ),
27
+ ("name", models.CharField(max_length=150)),
28
+ ("description", models.TextField(blank=True, null=True)),
29
+ ("is_applied", models.BooleanField(default=True)),
30
+ (
31
+ "country",
32
+ django_countries.fields.CountryField(
33
+ blank=True, default="KW", max_length=2, null=True
34
+ ),
35
+ ),
36
+ ("is_active", models.BooleanField(default=False)),
37
+ ("created_at", models.DateTimeField(auto_now_add=True)),
38
+ ("updated_at", models.DateTimeField(auto_now=True)),
39
+ ],
40
+ options={"verbose_name": "Tip", "verbose_name_plural": "Tips",},
41
+ ),
42
+ migrations.AddField(
43
+ model_name="order",
44
+ name="tip_percentage",
45
+ field=models.DecimalField(
46
+ blank=True,
47
+ decimal_places=2,
48
+ help_text="Percentage value of the tip (e.g., 10 for 10%)",
49
+ max_digits=5,
50
+ null=True,
51
+ ),
52
+ ),
53
+ migrations.AddField(
54
+ model_name="order",
55
+ name="tip_value",
56
+ field=models.DecimalField(
57
+ blank=True, decimal_places=2, max_digits=5, null=True
58
+ ),
59
+ ),
60
+ migrations.CreateModel(
61
+ name="TipAmount",
62
+ fields=[
63
+ (
64
+ "id",
65
+ models.AutoField(
66
+ auto_created=True,
67
+ primary_key=True,
68
+ serialize=False,
69
+ verbose_name="ID",
70
+ ),
71
+ ),
72
+ (
73
+ "percentage",
74
+ models.DecimalField(
75
+ blank=True,
76
+ decimal_places=2,
77
+ help_text="Percentage value of the tip (e.g., 10 for 10%)",
78
+ max_digits=5,
79
+ null=True,
80
+ ),
81
+ ),
82
+ (
83
+ "tip",
84
+ models.ForeignKey(
85
+ blank=True,
86
+ null=True,
87
+ on_delete=django.db.models.deletion.CASCADE,
88
+ related_name="tip_amounts",
89
+ to="stores.tip",
90
+ ),
91
+ ),
92
+ ],
93
+ options={
94
+ "verbose_name": "Tip Amount",
95
+ "verbose_name_plural": "Tip Amounts",
96
+ },
97
+ ),
98
+ ]
@@ -0,0 +1,20 @@
1
+ # Generated by Django 3.2.8 on 2025-11-25 14:24
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0112_auto_20251027_1739"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="order",
15
+ name="tax_value",
16
+ field=models.DecimalField(
17
+ blank=True, decimal_places=2, max_digits=5, null=True
18
+ ),
19
+ ),
20
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.8 on 2025-12-09 20:08
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0113_order_tax_value"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="store",
15
+ name="mask_customer_info",
16
+ field=models.BooleanField(default=False),
17
+ ),
18
+ ]
@@ -7,7 +7,13 @@ from ob_dj_store.core.stores.models._feedback import (
7
7
  FeedbackConfig,
8
8
  )
9
9
  from ob_dj_store.core.stores.models._inventory import Inventory, InventoryOperations
10
- from ob_dj_store.core.stores.models._order import Order, OrderHistory, OrderItem
10
+ from ob_dj_store.core.stores.models._order import (
11
+ Order,
12
+ OrderHistory,
13
+ OrderItem,
14
+ Tip,
15
+ TipAmount,
16
+ )
11
17
  from ob_dj_store.core.stores.models._partner import (
12
18
  Discount,
13
19
  Partner,
@@ -85,4 +91,6 @@ __all__ = [
85
91
  "PartnerOTPAuth",
86
92
  "Discount",
87
93
  "CountryPaymentMethod",
94
+ "Tip",
95
+ "TipAmount",
88
96
  ]
@@ -10,9 +10,7 @@ class BaseAddress(models.Model):
10
10
  Base class for all address models.
11
11
  """
12
12
 
13
- address_line = models.CharField(
14
- max_length=250,
15
- )
13
+ address_line = models.CharField(max_length=250,)
16
14
  address_line_arabic = models.CharField(max_length=250, blank=True, null=True)
17
15
  postal_code = models.CharField(
18
16
  max_length=64, help_text=_("The address postal/zip code.")
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from decimal import Decimal
2
3
 
3
4
  from django.contrib.auth import get_user_model
@@ -10,6 +11,8 @@ from ob_dj_store.core.stores.managers import CartItemManager, CartManager
10
11
  from ob_dj_store.core.stores.models._partner import PartnerAuthInfo
11
12
  from ob_dj_store.core.stores.utils import round_up_tie
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
 
14
17
  class Cart(models.Model):
15
18
  customer = models.OneToOneField(
@@ -48,7 +51,11 @@ class Cart(models.Model):
48
51
  def total_price(self) -> Decimal:
49
52
  total_price = Decimal(0)
50
53
  for item in self.items.all():
51
- total_price += item.total_price
54
+ try:
55
+ if item.inventory.quantity and item.inventory.quantity > 0:
56
+ total_price += item.total_price
57
+ except AttributeError:
58
+ logger.error(f"Item {item} has no inventory")
52
59
  return total_price
53
60
 
54
61
  @property
@@ -56,9 +63,7 @@ class Cart(models.Model):
56
63
  return self.total_price + self.get_applied_tax_amount()
57
64
 
58
65
  @property
59
- def full_price(
60
- self,
61
- ) -> Decimal:
66
+ def full_price(self,) -> Decimal:
62
67
  return self.total_price_with_tax - self.discount_offer_amount
63
68
 
64
69
  @property
@@ -90,7 +95,8 @@ class Cart(models.Model):
90
95
  def fill(self, order):
91
96
  from ob_dj_store.core.stores.models._cart import CartItem
92
97
 
93
- for item in order.items.all():
98
+ order_items = order.items.all()
99
+ for item in order_items:
94
100
  cart_item = CartItem.objects.create(
95
101
  cart=self,
96
102
  product_variant=item.product_variant,
@@ -22,9 +22,7 @@ class Feedback(models.Model):
22
22
  get_user_model(), related_name="feedbacks", on_delete=models.CASCADE
23
23
  )
24
24
  order = models.ForeignKey(
25
- "stores.Order",
26
- related_name="feedbacks",
27
- on_delete=models.CASCADE,
25
+ "stores.Order", related_name="feedbacks", on_delete=models.CASCADE,
28
26
  )
29
27
  review = models.CharField(
30
28
  max_length=100, choices=Reviews.choices, default="NOT_AVAILABLE"
@@ -37,8 +37,7 @@ class Inventory(DjangoModelCleanMixin, models.Model):
37
37
  # Add is_primary for variant
38
38
  is_primary = models.BooleanField(default=True)
39
39
  preparation_time = models.DurationField(
40
- default=timedelta(minutes=0),
41
- help_text=_("Preparation time in minutes"),
40
+ default=timedelta(minutes=0), help_text=_("Preparation time in minutes"),
42
41
  )
43
42
  created_at = models.DateTimeField(auto_now_add=True)
44
43
  updated_at = models.DateTimeField(auto_now=True)
@@ -61,6 +60,8 @@ class Inventory(DjangoModelCleanMixin, models.Model):
61
60
 
62
61
  @property
63
62
  def is_snoozed(self):
63
+ if not self.is_active:
64
+ return True
64
65
  return self.snooze_start_date <= now() <= self.snooze_end_date
65
66
 
66
67
  @property
@@ -1,12 +1,13 @@
1
- from decimal import Decimal
1
+ from decimal import ROUND_HALF_UP, Decimal
2
2
 
3
3
  from django.contrib.auth import get_user_model
4
4
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
5
5
  from django.core.validators import MinValueValidator
6
6
  from django.db import models
7
7
  from django.utils.translation import gettext_lazy as _
8
+ from django_countries.fields import CountryField
8
9
 
9
- from ob_dj_store.core.stores.managers import OrderItemManager, OrderManager
10
+ from ob_dj_store.core.stores.managers import OrderItemManager, OrderManager, TipManager
10
11
  from ob_dj_store.utils.model import DjangoModelCleanMixin
11
12
 
12
13
 
@@ -41,10 +42,7 @@ class Order(DjangoModelCleanMixin, models.Model):
41
42
  "stores.Discount", on_delete=models.PROTECT, null=True, blank=True
42
43
  )
43
44
  customer = models.ForeignKey(
44
- get_user_model(),
45
- related_name="orders",
46
- on_delete=models.SET_NULL,
47
- null=True,
45
+ get_user_model(), related_name="orders", on_delete=models.SET_NULL, null=True,
48
46
  )
49
47
  store = models.ForeignKey(
50
48
  "stores.Store",
@@ -82,9 +80,7 @@ class Order(DjangoModelCleanMixin, models.Model):
82
80
  blank=True,
83
81
  )
84
82
  status = models.CharField(
85
- max_length=32,
86
- default=OrderStatus.PENDING,
87
- choices=OrderStatus.choices,
83
+ max_length=32, default=OrderStatus.PENDING, choices=OrderStatus.choices,
88
84
  )
89
85
  # Add pickup time for an order, Pick up can be now or a later hour during the day
90
86
  pickup_time = models.DateTimeField(
@@ -93,12 +89,23 @@ class Order(DjangoModelCleanMixin, models.Model):
93
89
  # Order id of the pickup_car
94
90
  car_id = models.PositiveIntegerField(null=True, blank=True)
95
91
  # Pick up can be now or a later hour during the day. If pickup_time is not set,
96
- extra_infos = models.JSONField(
92
+ extra_infos = models.JSONField(null=True, blank=True,)
93
+ init_data = models.JSONField(null=True, blank=True)
94
+ offer_redeemed = models.BooleanField(default=False)
95
+ offer_id = models.IntegerField(null=True, blank=True, default=None)
96
+ tip_percentage = models.DecimalField(
97
+ max_digits=5,
98
+ decimal_places=2,
97
99
  null=True,
98
100
  blank=True,
101
+ help_text=_("Percentage value of the tip (e.g., 10 for 10%)"),
102
+ )
103
+ tip_value = models.DecimalField(
104
+ max_digits=5, decimal_places=2, null=True, blank=True,
105
+ )
106
+ tax_value = models.DecimalField(
107
+ max_digits=5, decimal_places=2, null=True, blank=True,
99
108
  )
100
- init_data = models.JSONField(null=True, blank=True)
101
-
102
109
  # TODO: add pick_up_time maybe ?
103
110
  # audit fields
104
111
  created_at = models.DateTimeField(auto_now_add=True)
@@ -163,6 +170,17 @@ class Order(DjangoModelCleanMixin, models.Model):
163
170
  return self.OrderType.WALLET.value
164
171
  return self.OrderType.PHYSICAL.value
165
172
 
173
+ def calculate_tip(self):
174
+ """
175
+ Returns the tip amount based on a total order amount.
176
+ """
177
+ if not self.tip_percentage:
178
+ return Decimal("0.00")
179
+ tip_value = (self.total_amount * self.tip_percentage / Decimal("100")).quantize(
180
+ Decimal("0.01"), rounding=ROUND_HALF_UP
181
+ )
182
+ return tip_value
183
+
166
184
  def save(self, **kwargs):
167
185
  if not self.pk and self.shipping_address:
168
186
  self.immutable_shipping_address = self.shipping_address.to_immutable()
@@ -195,10 +213,7 @@ class OrderItem(DjangoModelCleanMixin, models.Model):
195
213
  total_price = models.DecimalField(max_digits=10, decimal_places=3, default=0)
196
214
  quantity = models.PositiveIntegerField(
197
215
  validators=[
198
- MinValueValidator(
199
- 1,
200
- message="Can you please provide a valid quantity !",
201
- )
216
+ MinValueValidator(1, message="Can you please provide a valid quantity !",)
202
217
  ],
203
218
  help_text=_("quantity of the variant"),
204
219
  )
@@ -277,10 +292,7 @@ class OrderHistory(DjangoModelCleanMixin, models.Model):
277
292
  """
278
293
 
279
294
  order = models.ForeignKey(Order, related_name="history", on_delete=models.CASCADE)
280
- status = models.CharField(
281
- max_length=32,
282
- choices=Order.OrderStatus.choices,
283
- )
295
+ status = models.CharField(max_length=32, choices=Order.OrderStatus.choices,)
284
296
  created_at = models.DateTimeField(auto_now_add=True)
285
297
 
286
298
  class Meta:
@@ -290,3 +302,40 @@ class OrderHistory(DjangoModelCleanMixin, models.Model):
290
302
 
291
303
  def __str__(self):
292
304
  return f"OrderHistory - {self.status}"
305
+
306
+
307
+ class TipAmount(models.Model):
308
+ percentage = models.DecimalField(
309
+ max_digits=5,
310
+ decimal_places=2,
311
+ null=True,
312
+ blank=True,
313
+ help_text=_("Percentage value of the tip (e.g., 10 for 10%)"),
314
+ )
315
+ tip = models.ForeignKey(
316
+ "Tip",
317
+ related_name="tip_amounts",
318
+ on_delete=models.CASCADE,
319
+ null=True,
320
+ blank=True,
321
+ )
322
+
323
+ class Meta:
324
+ verbose_name = _("Tip Amount")
325
+ verbose_name_plural = _("Tip Amounts")
326
+
327
+
328
+ class Tip(models.Model):
329
+ name = models.CharField(max_length=150)
330
+ description = models.TextField(null=True, blank=True)
331
+ is_applied = models.BooleanField(default=True)
332
+ country = CountryField(blank=True, null=True, default="KW")
333
+ is_active = models.BooleanField(default=False)
334
+ created_at = models.DateTimeField(auto_now_add=True)
335
+ updated_at = models.DateTimeField(auto_now=True)
336
+
337
+ objects = TipManager()
338
+
339
+ class Meta:
340
+ verbose_name = _("Tip")
341
+ verbose_name_plural = _("Tips")
@@ -10,6 +10,7 @@ from django_countries.fields import CountryField
10
10
  from config import settings as store_settings
11
11
  from ob_dj_store.core.stores.managers import PaymentManager
12
12
  from ob_dj_store.core.stores.models import Order
13
+ from ob_dj_store.core.stores.utils import round_up_tie
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
@@ -35,8 +36,8 @@ class Tax(models.Model):
35
36
  country = CountryField(help_text=_("The address country."), default="KW")
36
37
  value = models.DecimalField(
37
38
  blank=True,
38
- max_digits=5,
39
- decimal_places=3,
39
+ max_digits=7,
40
+ decimal_places=5,
40
41
  null=True,
41
42
  help_text="Value for the given Payment -> 0.0625",
42
43
  )
@@ -75,21 +76,12 @@ class Payment(models.Model):
75
76
  related_name="user_payments",
76
77
  )
77
78
  status = models.CharField(
78
- max_length=100,
79
- default=PaymentStatus.INIT,
80
- choices=PaymentStatus.choices,
79
+ max_length=100, default=PaymentStatus.INIT, choices=PaymentStatus.choices,
81
80
  )
82
81
  method = models.ForeignKey(
83
- "stores.PaymentMethod",
84
- on_delete=models.CASCADE,
85
- null=True,
86
- blank=True,
87
- )
88
- payment_tax = models.ForeignKey(
89
- Tax,
90
- on_delete=models.SET_NULL,
91
- null=True,
82
+ "stores.PaymentMethod", on_delete=models.CASCADE, null=True, blank=True,
92
83
  )
84
+ payment_tax = models.ForeignKey(Tax, on_delete=models.SET_NULL, null=True,)
93
85
  orders = models.ManyToManyField("stores.Order", related_name="payments")
94
86
  amount = models.DecimalField(
95
87
  max_digits=settings.DEFAULT_MAX_DIGITS,
@@ -155,18 +147,26 @@ class Payment(models.Model):
155
147
  @property
156
148
  def total_payment(self):
157
149
  orders = self.orders.all()
158
- sum_orders = Decimal(
159
- sum(map(lambda order: Decimal(order.total_amount) or 0, orders))
150
+ sum_orders = sum(
151
+ (Decimal(order.total_amount) if order.total_amount else Decimal("0"))
152
+ for order in orders
160
153
  )
161
- if not self.payment_tax:
162
- return sum_orders
163
- elif not self.payment_tax.is_applied:
164
- return sum_orders
165
- elif self.payment_tax.rate == Tax.Rates.PERCENTAGE:
166
- perc = Decimal(sum_orders * self.payment_tax.value / 100)
167
- return round(sum_orders + perc, 3)
154
+ sum_orders = Decimal(sum_orders)
155
+
156
+ # Apply tax if applicable
157
+ tax = self.payment_tax
158
+ if tax and tax.is_applied:
159
+ if tax.rate == Tax.Rates.PERCENTAGE:
160
+ sum_orders += round_up_tie(sum_orders * Decimal(tax.value) / 100, 3)
161
+ else:
162
+ sum_orders += round_up_tie(self.payment_tax.value, 3)
163
+
164
+ # Add tip if present
165
+ first_order = orders.first()
166
+ if first_order and first_order.tip_value:
167
+ sum_orders += Decimal(first_order.tip_value)
168
168
 
169
- return round(sum_orders + self.payment_tax.value, 3)
169
+ return round_up_tie(sum_orders, 3)
170
170
 
171
171
  @property
172
172
  def type_of_order(self):
@@ -189,4 +189,8 @@ class Payment(models.Model):
189
189
  settings.BENEFIT,
190
190
  ]:
191
191
  payment_url = self.tap_payment.payment_url
192
+ elif gateway == settings.STRIPE:
193
+ # For Stripe, return the client_secret which is used by Stripe.js
194
+ if hasattr(self, "stripe_payment"):
195
+ payment_url = self.stripe_payment.payment_url
192
196
  return payment_url