ob-dj-store 0.0.15.18__py3-none-any.whl → 0.0.16.1__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.
@@ -17,7 +17,7 @@ from ob_dj_store.core.stores.models import (
17
17
  )
18
18
  from ob_dj_store.core.stores.models._inventory import Inventory
19
19
  from ob_dj_store.core.stores.models._wallet import Wallet
20
- from ob_dj_store.core.stores.utils import validate_currency
20
+ from ob_dj_store.core.stores.utils import get_currency_by_country, validate_currency
21
21
 
22
22
 
23
23
  class StoreFilter(filters.FilterSet):
@@ -268,9 +268,15 @@ class WalletFilter(filters.FilterSet):
268
268
  validate_currency,
269
269
  ],
270
270
  )
271
+ country = filters.CharFilter(method="by_country")
271
272
 
272
273
  class Meta:
273
274
  models = Wallet
274
275
 
275
276
  def by_currency(self, queryset, name, value):
276
277
  return queryset.filter(currency=value)
278
+
279
+ def by_country(self, queryset, name, value):
280
+ if value:
281
+ currency = get_currency_by_country(value)
282
+ return queryset.filter(currency=currency)
@@ -16,6 +16,7 @@ from django.shortcuts import get_object_or_404
16
16
  from django.utils.module_loading import import_string
17
17
  from django.utils.timezone import localtime, now
18
18
  from django.utils.translation import gettext_lazy as _
19
+ from ob_dj_otp.core.otp.models import OneTruePairing
19
20
  from phonenumber_field.phonenumber import to_python
20
21
  from rest_framework import serializers
21
22
 
@@ -35,6 +36,9 @@ from ob_dj_store.core.stores.models import (
35
36
  Order,
36
37
  OrderHistory,
37
38
  OrderItem,
39
+ Partner,
40
+ PartnerAuthInfo,
41
+ PartnerOTPAuth,
38
42
  Payment,
39
43
  PaymentMethod,
40
44
  PhoneContact,
@@ -51,7 +55,7 @@ from ob_dj_store.core.stores.models import (
51
55
  WalletTransaction,
52
56
  )
53
57
  from ob_dj_store.core.stores.models._inventory import Inventory
54
- from ob_dj_store.core.stores.utils import distance
58
+ from ob_dj_store.core.stores.utils import PartnerAuth, distance
55
59
 
56
60
  logger = logging.getLogger(__name__)
57
61
 
@@ -176,6 +180,7 @@ class OrderSerializer(serializers.ModelSerializer):
176
180
  "total_amount",
177
181
  "preparation_time",
178
182
  "estimated_timeline",
183
+ "get_discount_amount",
179
184
  "history",
180
185
  "car_id",
181
186
  "pickup_time",
@@ -653,6 +658,8 @@ class CartSerializer(serializers.ModelSerializer):
653
658
  "total_price",
654
659
  "tax_amount",
655
660
  "total_price_with_tax",
661
+ "discount_offer_amount",
662
+ "total_price_with_discount",
656
663
  )
657
664
  read_only_fields = (
658
665
  "id",
@@ -1193,6 +1200,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
1193
1200
  "content_object",
1194
1201
  "extras",
1195
1202
  "object_id",
1203
+ "extra_info",
1196
1204
  "object_type",
1197
1205
  "name",
1198
1206
  "is_available_in_store",
@@ -1309,6 +1317,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
1309
1317
  "content_object": object_instance,
1310
1318
  "extras": extras,
1311
1319
  "name": validated_data["name"],
1320
+ "extra_info": validated_data.get("extra_info", None),
1312
1321
  }
1313
1322
  self._lookup_validation(validated_data)
1314
1323
  return validated_data
@@ -1319,6 +1328,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
1319
1328
  content_object=validated_data["content_object"],
1320
1329
  user=self.context["request"].user,
1321
1330
  name=validated_data["name"],
1331
+ extra_info=validated_data["extra_info"],
1322
1332
  extras=validated_data["extras"],
1323
1333
  )
1324
1334
  except ValidationError as e:
@@ -1329,6 +1339,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
1329
1339
  try:
1330
1340
  favorite = instance.update_favorite(
1331
1341
  validated_data["name"],
1342
+ validated_data["extra_info"],
1332
1343
  validated_data["extras"],
1333
1344
  )
1334
1345
  except ValidationError as e:
@@ -1456,3 +1467,149 @@ class OrderDataSerializer(serializers.ModelSerializer):
1456
1467
 
1457
1468
  def get_shipping_method_name(self, obj):
1458
1469
  return obj.shipping_method.name if obj.shipping_method else None
1470
+
1471
+
1472
+ class PartnerOTPRequestSerializer(serializers.ModelSerializer):
1473
+ email = serializers.EmailField(required=True)
1474
+
1475
+ class Meta:
1476
+ model = PartnerOTPAuth
1477
+ exclude = ("user", "otp", "partner")
1478
+
1479
+ def _validate_old_valid_verification_code(self, attrs):
1480
+ # Validate there is unused OTP code
1481
+ timeout = now() - timedelta(seconds=getattr(settings, "OTP_TIMEOUT", 3 * 60))
1482
+ qs = PartnerOTPAuth.objects.filter(
1483
+ created_at__gte=timeout, email=attrs["email"]
1484
+ )
1485
+ if qs.exists():
1486
+ # TODO: Add a mechanism to force a new code request
1487
+ # with a field called "force" with checking that he didn't call this api to many times
1488
+ seconds_left = (
1489
+ timedelta(seconds=getattr(settings, "OTP_TIMEOUT", 3 * 60))
1490
+ - (now() - qs.last().created_at)
1491
+ ).total_seconds()
1492
+ minutes_left = int(seconds_left) // 60 or 1
1493
+ raise serializers.ValidationError(
1494
+ _(
1495
+ "We sent a verification code please wait for "
1496
+ "{minutes} minutes; before requesting a new code."
1497
+ ).format(minutes=minutes_left)
1498
+ )
1499
+
1500
+ def validate(self, attrs):
1501
+ attrs = super().validate(attrs)
1502
+ try:
1503
+ partner_auth = PartnerAuth(attrs["email"])
1504
+ except ValidationError as e:
1505
+ raise serializers.ValidationError(detail=e.messages)
1506
+ partner = partner_auth.partner
1507
+ if partner.auth_method != Partner.AuthMethods.OTP:
1508
+ raise serializers.ValidationError(
1509
+ _(f"{partner.name} does not support OTP as Auth Method")
1510
+ )
1511
+ self._validate_old_valid_verification_code(attrs)
1512
+ attrs["partner"] = partner
1513
+ return attrs
1514
+
1515
+ def create(self, validated_data):
1516
+ user = self.context["view"].request.user
1517
+ otp = OneTruePairing.objects.create(
1518
+ user=user, usage=OneTruePairing.Usages.auth, email=validated_data["email"]
1519
+ )
1520
+ partner_otp_auth = PartnerOTPAuth.objects.create(
1521
+ user=self.context["view"].request.user,
1522
+ partner=validated_data["partner"],
1523
+ otp=otp,
1524
+ email=validated_data["email"],
1525
+ )
1526
+ return partner_otp_auth
1527
+
1528
+
1529
+ class PartnerAuthInfoSerializer(serializers.ModelSerializer):
1530
+ promotion_code = serializers.IntegerField(write_only=True, required=False)
1531
+ otp_code = serializers.IntegerField(write_only=True, required=False)
1532
+ email = serializers.EmailField(write_only=True, required=True)
1533
+
1534
+ class Meta:
1535
+ model = PartnerAuthInfo
1536
+ fields = (
1537
+ "id",
1538
+ "user",
1539
+ "email",
1540
+ "otp_code",
1541
+ "promotion_code",
1542
+ "partner",
1543
+ "authentication_expires",
1544
+ "created_at",
1545
+ "updated_at",
1546
+ )
1547
+ extra_kwargs = {
1548
+ "user": {"read_only": True},
1549
+ "email": {"read_only": True},
1550
+ "partner": {"read_only": True},
1551
+ "authentication_expires": {"read_only": True},
1552
+ }
1553
+
1554
+ def _validate_partner_auth_method(self, attrs):
1555
+ partner = attrs["partner"]
1556
+ user = self.context["view"].request.user
1557
+ if partner.auth_method == Partner.AuthMethods.OTP:
1558
+ otp_code = attrs.get("otp_code", None)
1559
+ if otp_code == None:
1560
+ raise serializers.ValidationError(_("OTP code is required"))
1561
+ timeout = now() - timedelta(
1562
+ seconds=getattr(settings, "OTP_TIMEOUT", 3 * 60)
1563
+ )
1564
+ try:
1565
+ otp = OneTruePairing.objects.filter(
1566
+ verification_code=otp_code,
1567
+ status=OneTruePairing.Statuses.init,
1568
+ created_at__gte=timeout,
1569
+ ).get(
1570
+ email=attrs["email"],
1571
+ partner_otp_auth__partner=partner,
1572
+ user=user,
1573
+ )
1574
+ self.context["otp"] = otp
1575
+ except ObjectDoesNotExist as e:
1576
+ raise serializers.ValidationError(
1577
+ _(f"Invalid verification code.")
1578
+ ) from e
1579
+
1580
+ elif partner.auth_method == Partner.AuthMethods.CODE:
1581
+ promotion_code = attrs.get("promotion_code", None)
1582
+ if promotion_code == None:
1583
+ raise serializers.ValidationError(_("Promotion code is required"))
1584
+ elif not partner.promotion_code:
1585
+ logger.error(f"{partner.name} does not have promotion code")
1586
+ raise serializers.ValidationError(_("Promotion code is not working"))
1587
+ elif partner.promotion_code != promotion_code:
1588
+ raise serializers.ValidationError(_("Promotion code is invalid"))
1589
+ return attrs
1590
+
1591
+ def validate(self, attrs):
1592
+ attrs = super().validate(attrs)
1593
+ try:
1594
+ partner_auth = PartnerAuth(attrs["email"])
1595
+ except ValidationError as e:
1596
+ raise serializers.ValidationError(detail=e.messages)
1597
+ attrs["partner"] = partner_auth.partner
1598
+ self._validate_partner_auth_method(attrs)
1599
+ return attrs
1600
+
1601
+ def create(self, validated_data):
1602
+ user = self.context["view"].request.user
1603
+ partner = validated_data["partner"]
1604
+ if partner.auth_method == Partner.AuthMethods.OTP:
1605
+ otp = self.context["otp"]
1606
+ otp.status = OneTruePairing.Statuses.used
1607
+ otp.save()
1608
+ partner_auth_info = PartnerAuthInfo.objects.update_or_create(
1609
+ user=user,
1610
+ defaults={
1611
+ "partner": partner,
1612
+ "email": validated_data["email"],
1613
+ },
1614
+ )
1615
+ return partner_auth_info
@@ -9,6 +9,7 @@ from ob_dj_store.apis.stores.views import (
9
9
  FavoriteViewSet,
10
10
  InventoryView,
11
11
  OrderView,
12
+ PartnerAuthInfoViewSet,
12
13
  PaymentMethodViewSet,
13
14
  ProductView,
14
15
  ReorderViewSet,
@@ -42,6 +43,7 @@ router.register(r"favorite", FavoriteViewSet, basename="favorite")
42
43
  router.register(r"wallet", WalletViewSet, basename="wallet")
43
44
  router.register(r"payment-method", PaymentMethodViewSet, basename="payment-method")
44
45
  router.register(r"order", ReorderViewSet, basename="re-order")
46
+ router.register(r"partner/auth", PartnerAuthInfoViewSet, basename="auth")
45
47
  urlpatterns = [
46
48
  path(r"", include(router.urls)),
47
49
  path(r"", include(stores_router.urls)),
@@ -41,6 +41,8 @@ from ob_dj_store.apis.stores.rest.serializers.serializers import (
41
41
  FeedbackSerializer,
42
42
  InventorySerializer,
43
43
  OrderSerializer,
44
+ PartnerAuthInfoSerializer,
45
+ PartnerOTPRequestSerializer,
44
46
  PaymentMethodSerializer,
45
47
  PaymentSerializer,
46
48
  ProductListSerializer,
@@ -65,6 +67,7 @@ from ob_dj_store.core.stores.models import (
65
67
  Favorite,
66
68
  FeedbackConfig,
67
69
  Order,
70
+ PartnerAuthInfo,
68
71
  Payment,
69
72
  PaymentMethod,
70
73
  PhoneContact,
@@ -1259,3 +1262,69 @@ class WalletViewSet(
1259
1262
 
1260
1263
  serializer = self.get_serializer(queryset, many=True)
1261
1264
  return Response(serializer.data)
1265
+
1266
+
1267
+ class PartnerAuthInfoViewSet(
1268
+ mixins.CreateModelMixin,
1269
+ viewsets.GenericViewSet,
1270
+ ):
1271
+ queryset = PartnerAuthInfo.objects.all()
1272
+ permission_classes = [
1273
+ permissions.IsAuthenticated,
1274
+ ]
1275
+
1276
+ @swagger_auto_schema(
1277
+ operation_summary="Send an OTP for PartnerAuthInfo",
1278
+ operation_description="""
1279
+ Send an OTP for PartnerAuthInfo
1280
+ """,
1281
+ tags=[
1282
+ "Partner",
1283
+ ],
1284
+ )
1285
+ @action(
1286
+ methods=[
1287
+ "POST",
1288
+ ],
1289
+ detail=False,
1290
+ url_path="send-otp",
1291
+ serializer_class=PartnerOTPRequestSerializer,
1292
+ )
1293
+ def send_otp(
1294
+ self, request: Request, *args: typing.Any, **kwargs: typing.Any
1295
+ ) -> Response:
1296
+ serializer = self.get_serializer(data=request.data)
1297
+ serializer.is_valid(raise_exception=True)
1298
+ serializer.save()
1299
+ headers = self.get_success_headers(serializer.data)
1300
+ return Response(
1301
+ {"result": "ok"}, status=status.HTTP_201_CREATED, headers=headers
1302
+ )
1303
+
1304
+ @swagger_auto_schema(
1305
+ operation_summary="verify Partner's Authentication ",
1306
+ operation_description="""
1307
+ verify Partner's Authentication for different auth methods
1308
+ """,
1309
+ tags=[
1310
+ "Partner",
1311
+ ],
1312
+ )
1313
+ @action(
1314
+ methods=[
1315
+ "POST",
1316
+ ],
1317
+ detail=False,
1318
+ url_path="",
1319
+ serializer_class=PartnerAuthInfoSerializer,
1320
+ )
1321
+ def verify(
1322
+ self, request: Request, *args: typing.Any, **kwargs: typing.Any
1323
+ ) -> Response:
1324
+ serializer = self.get_serializer(data=request.data)
1325
+ serializer.is_valid(raise_exception=True)
1326
+ serializer.save()
1327
+ headers = self.get_success_headers(serializer.data)
1328
+ return Response(
1329
+ serializer.data, status=status.HTTP_201_CREATED, headers=headers
1330
+ )
@@ -1,4 +1,5 @@
1
1
  import typing
2
+ from typing import Any
2
3
 
3
4
  from django import forms
4
5
  from django.contrib import admin
@@ -16,6 +17,7 @@ from ob_dj_store.core.stores.admin_inlines import (
16
17
  OpeningHoursInlineAdmin,
17
18
  OrderHistoryInlineAdmin,
18
19
  OrderItemInline,
20
+ PartnerEmailDomainInlineAdmin,
19
21
  PhoneContactInlineAdmin,
20
22
  ProductAttributeInlineAdmin,
21
23
  ProductMediaInlineAdmin,
@@ -386,6 +388,11 @@ class WalletTransactionAdmin(ImportExportModelAdmin, admin.ModelAdmin):
386
388
  def user(self, obj) -> typing.Text:
387
389
  return obj.wallet.user.email
388
390
 
391
+ def save_model(self, request: Any, obj: Any, form: Any, change: Any) -> None:
392
+ if not obj.pk:
393
+ obj.is_by_admin = True
394
+ return super().save_model(request, obj, form, change)
395
+
389
396
 
390
397
  class WalletAdmin(admin.ModelAdmin):
391
398
  list_display = ("id", "user", "balance", "currency")
@@ -402,6 +409,35 @@ class AvailabilityHoursAdmin(admin.ModelAdmin):
402
409
  list_display = ("id", "store", "category", "from_hour", "to_hour")
403
410
 
404
411
 
412
+ # Partner Admins
413
+ class PartnerAdmin(admin.ModelAdmin):
414
+ list_display = ("name", "auth_method", "country", "discount", "promotion_code")
415
+ search_fields = ["name", "country", "domains__email_domain"]
416
+ list_filter = ["discount", "auth_method"]
417
+ inlines = [
418
+ PartnerEmailDomainInlineAdmin,
419
+ ]
420
+
421
+
422
+ class DiscountAdmin(admin.ModelAdmin):
423
+ list_display = ("id", "discount_rate", "is_active")
424
+
425
+
426
+ class PartnerAuthInfoAdmin(admin.ModelAdmin):
427
+ list_display = (
428
+ "id",
429
+ "user",
430
+ "email",
431
+ "partner",
432
+ "authentication_expires",
433
+ "created_at",
434
+ )
435
+ search_fields = ["email", "user__email", "partner__name"]
436
+ list_filter = [
437
+ "authentication_expires",
438
+ ]
439
+
440
+
405
441
  admin.site.register(models.Store, StoreAdmin)
406
442
  admin.site.register(models.ShippingMethod, ShippingMethodAdmin)
407
443
  admin.site.register(models.PaymentMethod, PaymentMethodAdmin)
@@ -422,3 +458,6 @@ admin.site.register(models.Wallet, WalletAdmin)
422
458
  admin.site.register(models.WalletMedia, WalletMediaAdmin)
423
459
  admin.site.register(models.AvailabilityHours, AvailabilityHoursAdmin)
424
460
  admin.site.register(models.Menu, MenuAdmin)
461
+ admin.site.register(models.Partner, PartnerAdmin)
462
+ admin.site.register(models.Discount, DiscountAdmin)
463
+ admin.site.register(models.PartnerAuthInfo, PartnerAuthInfoAdmin)
@@ -118,3 +118,8 @@ class OrderItemInline(admin.TabularInline):
118
118
  class InventoryOperationInlineAdmin(admin.TabularInline):
119
119
  model = models.InventoryOperations
120
120
  extra = 1
121
+
122
+
123
+ class PartnerEmailDomainInlineAdmin(admin.TabularInline):
124
+ model = models.PartnerEmailDomain
125
+ extra = 1
@@ -6,6 +6,7 @@ from decimal import Decimal
6
6
  from django.contrib.contenttypes.models import ContentType
7
7
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
8
8
  from django.db import models
9
+ from django.utils.timezone import now
9
10
  from django.utils.translation import gettext_lazy as _
10
11
 
11
12
  from config import settings
@@ -63,7 +64,7 @@ class PaymentManager(models.Manager):
63
64
  {"order", _("You cannot perform payment without items")}
64
65
  )
65
66
  try:
66
- if orders[0].store:
67
+ if orders[0].store and orders[0].type_of_order == "PHYSICAL":
67
68
  kwargs["payment_tax"] = Tax.objects.get(
68
69
  country=orders[0].store.address.country, is_active=True
69
70
  )
@@ -150,7 +151,21 @@ class OrderManager(models.Manager):
150
151
  from ob_dj_store.apis.stores.rest.serializers.serializers import (
151
152
  OrderDataSerializer,
152
153
  )
154
+ from ob_dj_store.core.stores.models._partner import PartnerAuthInfo
153
155
 
156
+ try:
157
+ partner_auth_info = PartnerAuthInfo.objects.get(
158
+ user=kwargs["customer"],
159
+ authentication_expires__gte=now(),
160
+ partner__offer_start_time__lte=now(),
161
+ partner__offer_end_time__gt=now(),
162
+ )
163
+ except ObjectDoesNotExist:
164
+ partner_auth_info = None
165
+ if partner_auth_info:
166
+ partner = partner_auth_info.partner
167
+ if partner.stores.filter(pk=kwargs["store"].pk).exists():
168
+ kwargs["discount"] = partner.discount
154
169
  order = super().create(**kwargs)
155
170
  serializer = OrderDataSerializer(order)
156
171
  order.init_data = serializer.data
@@ -254,3 +269,8 @@ class WalletTransactionManager(models.Manager):
254
269
  if type == WalletTransaction.TYPE.DEBIT and wallet.balance < kwargs["amount"]:
255
270
  raise ValidationError(_("Insufficient Funds"))
256
271
  return super().create(*args, **kwargs)
272
+
273
+
274
+ class PartnerAuthInfoManager(models.Manager):
275
+ def create(self, *args, **kwargs):
276
+ return super().create(*args, **kwargs)
@@ -0,0 +1,208 @@
1
+ # Generated by Django 3.2.8 on 2023-10-04 11:14
2
+
3
+ import django.core.validators
4
+ import django.db.models.deletion
5
+ import django_countries.fields
6
+ from django.conf import settings
7
+ from django.db import migrations, models
8
+
9
+ import ob_dj_store.utils.model
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+
14
+ dependencies = [
15
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
+ ("otp", "0004_onetruepairing_meta"),
17
+ ("stores", "0088_alter_paymentmethod_payment_provider"),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.CreateModel(
22
+ name="Discount",
23
+ fields=[
24
+ (
25
+ "id",
26
+ models.AutoField(
27
+ auto_created=True,
28
+ primary_key=True,
29
+ serialize=False,
30
+ verbose_name="ID",
31
+ ),
32
+ ),
33
+ (
34
+ "discount_rate",
35
+ models.DecimalField(
36
+ decimal_places=2,
37
+ max_digits=3,
38
+ validators=[
39
+ django.core.validators.MaxValueValidator(limit_value=1)
40
+ ],
41
+ ),
42
+ ),
43
+ ("is_active", models.BooleanField(default=True)),
44
+ ("created_at", models.DateTimeField(auto_now_add=True)),
45
+ ("updated_at", models.DateTimeField(auto_now=True)),
46
+ ],
47
+ ),
48
+ migrations.CreateModel(
49
+ name="Partner",
50
+ fields=[
51
+ (
52
+ "id",
53
+ models.AutoField(
54
+ auto_created=True,
55
+ primary_key=True,
56
+ serialize=False,
57
+ verbose_name="ID",
58
+ ),
59
+ ),
60
+ (
61
+ "name",
62
+ models.CharField(max_length=255, verbose_name="Partner's Name"),
63
+ ),
64
+ (
65
+ "promotion_code",
66
+ models.PositiveBigIntegerField(
67
+ blank=True, null=True, verbose_name="Promotion code"
68
+ ),
69
+ ),
70
+ (
71
+ "auth_method",
72
+ models.CharField(
73
+ choices=[
74
+ ("OTP", "One True Pairing"),
75
+ ("CODE", "Promotion code"),
76
+ ],
77
+ max_length=255,
78
+ verbose_name="Authentication method",
79
+ ),
80
+ ),
81
+ (
82
+ "country",
83
+ django_countries.fields.CountryField(
84
+ help_text="Partner's country.", max_length=2
85
+ ),
86
+ ),
87
+ ("created_at", models.DateTimeField(auto_now_add=True)),
88
+ ("updated_at", models.DateTimeField(auto_now=True)),
89
+ (
90
+ "discount",
91
+ models.ForeignKey(
92
+ on_delete=django.db.models.deletion.PROTECT,
93
+ related_name="partners",
94
+ to="stores.discount",
95
+ ),
96
+ ),
97
+ ],
98
+ bases=(ob_dj_store.utils.model.DjangoModelCleanMixin, models.Model),
99
+ ),
100
+ migrations.CreateModel(
101
+ name="PartnerOTPAuth",
102
+ fields=[
103
+ (
104
+ "id",
105
+ models.AutoField(
106
+ auto_created=True,
107
+ primary_key=True,
108
+ serialize=False,
109
+ verbose_name="ID",
110
+ ),
111
+ ),
112
+ (
113
+ "email",
114
+ models.EmailField(max_length=254, verbose_name="Partner's Email"),
115
+ ),
116
+ ("created_at", models.DateTimeField(auto_now_add=True)),
117
+ ("updated_at", models.DateTimeField(auto_now=True)),
118
+ (
119
+ "otp",
120
+ models.OneToOneField(
121
+ on_delete=django.db.models.deletion.PROTECT,
122
+ to="otp.onetruepairing",
123
+ ),
124
+ ),
125
+ (
126
+ "partner",
127
+ models.ForeignKey(
128
+ on_delete=django.db.models.deletion.CASCADE,
129
+ related_name="otp_auths",
130
+ to="stores.partner",
131
+ ),
132
+ ),
133
+ (
134
+ "user",
135
+ models.ForeignKey(
136
+ on_delete=django.db.models.deletion.CASCADE,
137
+ to=settings.AUTH_USER_MODEL,
138
+ ),
139
+ ),
140
+ ],
141
+ ),
142
+ migrations.CreateModel(
143
+ name="PartnerEmailDomain",
144
+ fields=[
145
+ (
146
+ "id",
147
+ models.AutoField(
148
+ auto_created=True,
149
+ primary_key=True,
150
+ serialize=False,
151
+ verbose_name="ID",
152
+ ),
153
+ ),
154
+ ("email_domain", models.CharField(max_length=255)),
155
+ ("created_at", models.DateTimeField(auto_now_add=True)),
156
+ ("updated_at", models.DateTimeField(auto_now=True)),
157
+ (
158
+ "partner",
159
+ models.ForeignKey(
160
+ on_delete=django.db.models.deletion.CASCADE,
161
+ related_name="domains",
162
+ to="stores.partner",
163
+ ),
164
+ ),
165
+ ],
166
+ bases=(ob_dj_store.utils.model.DjangoModelCleanMixin, models.Model),
167
+ ),
168
+ migrations.CreateModel(
169
+ name="PartnerAuthInfo",
170
+ fields=[
171
+ (
172
+ "id",
173
+ models.AutoField(
174
+ auto_created=True,
175
+ primary_key=True,
176
+ serialize=False,
177
+ verbose_name="ID",
178
+ ),
179
+ ),
180
+ (
181
+ "email",
182
+ models.EmailField(max_length=254, verbose_name="Partner's Email"),
183
+ ),
184
+ ("authentication_details", models.JSONField(blank=True, null=True)),
185
+ ("authentication_expires", models.DateTimeField()),
186
+ ("created_at", models.DateTimeField(auto_now_add=True)),
187
+ (
188
+ "updated_at",
189
+ models.DateTimeField(
190
+ auto_now=True, verbose_name="Last Authentifacation"
191
+ ),
192
+ ),
193
+ (
194
+ "partner",
195
+ models.ForeignKey(
196
+ on_delete=django.db.models.deletion.CASCADE, to="stores.partner"
197
+ ),
198
+ ),
199
+ (
200
+ "user",
201
+ models.OneToOneField(
202
+ on_delete=django.db.models.deletion.CASCADE,
203
+ to=settings.AUTH_USER_MODEL,
204
+ ),
205
+ ),
206
+ ],
207
+ ),
208
+ ]
@@ -0,0 +1,32 @@
1
+ # Generated by Django 3.2.8 on 2023-10-06 10:56
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
+ ("otp", "0004_onetruepairing_meta"),
11
+ (
12
+ "stores",
13
+ "0089_discount_partner_partnerauthinfo_partneremaildomain_partnerotpauth",
14
+ ),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.AddField(
19
+ model_name="favorite",
20
+ name="extra_info",
21
+ field=models.TextField(blank=True, null=True),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name="partnerotpauth",
25
+ name="otp",
26
+ field=models.OneToOneField(
27
+ on_delete=django.db.models.deletion.PROTECT,
28
+ related_name="partner_otp_auth",
29
+ to="otp.onetruepairing",
30
+ ),
31
+ ),
32
+ ]
@@ -0,0 +1,30 @@
1
+ # Generated by Django 3.2.8 on 2023-10-10 14:40
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0090_auto_20231006_1356"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="partner",
15
+ name="stores",
16
+ field=models.ManyToManyField(related_name="partners", to="stores.Store"),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name="partner",
20
+ name="promotion_code",
21
+ field=models.PositiveBigIntegerField(
22
+ blank=True, null=True, unique=True, verbose_name="Promotion code"
23
+ ),
24
+ ),
25
+ migrations.AlterField(
26
+ model_name="partneremaildomain",
27
+ name="email_domain",
28
+ field=models.CharField(max_length=255, unique=True),
29
+ ),
30
+ ]
@@ -0,0 +1,47 @@
1
+ # Generated by Django 3.2.8 on 2023-10-12 10:39
2
+
3
+ import django.db.models.deletion
4
+ import django.utils.timezone
5
+ from django.db import migrations, models
6
+
7
+ import ob_dj_store.core.stores.models._partner
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ dependencies = [
13
+ ("stores", "0091_auto_20231010_1740"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AddField(
18
+ model_name="order",
19
+ name="discount",
20
+ field=models.ForeignKey(
21
+ blank=True,
22
+ null=True,
23
+ on_delete=django.db.models.deletion.PROTECT,
24
+ to="stores.discount",
25
+ ),
26
+ ),
27
+ migrations.AddField(
28
+ model_name="partner",
29
+ name="offer_end_time",
30
+ field=models.DateTimeField(
31
+ default=ob_dj_store.core.stores.models._partner.default_offer_end_time,
32
+ verbose_name="Offer end date",
33
+ ),
34
+ ),
35
+ migrations.AddField(
36
+ model_name="partner",
37
+ name="offer_start_time",
38
+ field=models.DateTimeField(
39
+ default=django.utils.timezone.now, verbose_name="Offer start date"
40
+ ),
41
+ ),
42
+ migrations.AddField(
43
+ model_name="wallettransaction",
44
+ name="is_by_admin",
45
+ field=models.BooleanField(default=False),
46
+ ),
47
+ ]
@@ -8,6 +8,13 @@ from ob_dj_store.core.stores.models._feedback import (
8
8
  )
9
9
  from ob_dj_store.core.stores.models._inventory import Inventory, InventoryOperations
10
10
  from ob_dj_store.core.stores.models._order import Order, OrderHistory, OrderItem
11
+ from ob_dj_store.core.stores.models._partner import (
12
+ Discount,
13
+ Partner,
14
+ PartnerAuthInfo,
15
+ PartnerEmailDomain,
16
+ PartnerOTPAuth,
17
+ )
11
18
  from ob_dj_store.core.stores.models._payment import Payment, Tax
12
19
  from ob_dj_store.core.stores.models._product import (
13
20
  Attribute,
@@ -71,4 +78,9 @@ __all__ = [
71
78
  "AvailabilityHours",
72
79
  "StoreAttributeChoice",
73
80
  "Menu",
81
+ "PartnerEmailDomain",
82
+ "Partner",
83
+ "PartnerAuthInfo",
84
+ "PartnerOTPAuth",
85
+ "Discount",
74
86
  ]
@@ -3,9 +3,11 @@ from decimal import Decimal
3
3
  from django.contrib.auth import get_user_model
4
4
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
5
5
  from django.db import models
6
+ from django.utils.timezone import now
6
7
  from django.utils.translation import gettext_lazy as _
7
8
 
8
9
  from ob_dj_store.core.stores.managers import CartItemManager, CartManager
10
+ from ob_dj_store.core.stores.models._partner import PartnerAuthInfo
9
11
 
10
12
 
11
13
  class Cart(models.Model):
@@ -43,6 +45,27 @@ class Cart(models.Model):
43
45
  def total_price_with_tax(self) -> Decimal:
44
46
  return self.total_price + self.tax_amount
45
47
 
48
+ @property
49
+ def get_user_partner(self):
50
+ try:
51
+ partner_auth_info = PartnerAuthInfo.objects.get(
52
+ user=self.customer,
53
+ authentication_expires__gte=now(),
54
+ partner__offer_start_time__lte=now(),
55
+ partner__offer_end_time__gt=now(),
56
+ )
57
+ return partner_auth_info.partner
58
+ except ObjectDoesNotExist:
59
+ return None
60
+
61
+ @property
62
+ def discount_offer_amount(self):
63
+ return sum(map(lambda item: item.discount_amount, self.items.all()))
64
+
65
+ @property
66
+ def total_price_with_discount(self):
67
+ return self.total_price - self.discount_offer_amount
68
+
46
69
  def __str__(self):
47
70
  return f"Cart - {self.customer.email} with total price {self.total_price}"
48
71
 
@@ -124,6 +147,14 @@ class CartItem(models.Model):
124
147
  pass
125
148
  return 0
126
149
 
150
+ @property
151
+ def discount_amount(self):
152
+ partner = self.cart.get_user_partner
153
+ if partner:
154
+ if partner.discount and partner.stores.filter(pk=self.store.pk).exists():
155
+ return partner.discount.perc_to_flat(self.total_price)
156
+ return Decimal(0)
157
+
127
158
  @property
128
159
  def attribute_choices_total_price(self) -> Decimal:
129
160
  total_price = Decimal(0)
@@ -20,6 +20,7 @@ class Favorite(DjangoModelCleanMixin, models.Model):
20
20
  content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
21
21
  content_object = GenericForeignKey("content_type", "object_id")
22
22
  object_id = models.PositiveIntegerField()
23
+ extra_info = models.TextField(blank=True, null=True)
23
24
  # Audit fields
24
25
  created_on = models.DateTimeField(auto_now_add=True)
25
26
  objects = FavoriteManager()
@@ -33,13 +34,14 @@ class Favorite(DjangoModelCleanMixin, models.Model):
33
34
  return f"{self.user} favorites {self.content_object}"
34
35
 
35
36
  @classmethod
36
- def add_favorite(cls, content_object, user, name, extras=[]):
37
+ def add_favorite(cls, content_object, user, name, extra_info=None, extras=[]):
37
38
  content_type = ContentType.objects.get_for_model(type(content_object))
38
39
  favorite = Favorite(
39
40
  user=user,
40
41
  content_type=content_type,
41
42
  object_id=content_object.pk,
42
43
  content_object=content_object,
44
+ extra_info=extra_info,
43
45
  name=name,
44
46
  )
45
47
  favorite.save()
@@ -52,10 +54,12 @@ class Favorite(DjangoModelCleanMixin, models.Model):
52
54
  )
53
55
  return favorite
54
56
 
55
- def update_favorite(self, name, extras=[]):
57
+ def update_favorite(self, name, extra_info=None, extras=[]):
56
58
  from ob_dj_store.core.stores.models._favorite import FavoriteExtra
57
59
 
58
60
  self.name = name
61
+ if extra_info:
62
+ self.extra_info = extra_info
59
63
  self.save()
60
64
  FavoriteExtra.objects.filter(favorite=self).delete()
61
65
  for extra in extras:
@@ -37,6 +37,9 @@ class Order(DjangoModelCleanMixin, models.Model):
37
37
  # TODO: Probably we want to setup the on_delete to SET_NULL because orders is part of
38
38
  # sales and even if a user deleted orders cannot disappear otherwise will reflect
39
39
  # invalid sales figure; same can be applied for the store field
40
+ discount = models.ForeignKey(
41
+ "stores.Discount", on_delete=models.PROTECT, null=True, blank=True
42
+ )
40
43
  customer = models.ForeignKey(
41
44
  get_user_model(),
42
45
  related_name="orders",
@@ -126,6 +129,10 @@ class Order(DjangoModelCleanMixin, models.Model):
126
129
  self.status = Order.OrderStatus.READY
127
130
  self.save()
128
131
 
132
+ @property
133
+ def get_discount_amount(self):
134
+ return sum(map(lambda item: item.discount_offer_amount, self.items.all()))
135
+
129
136
  @property
130
137
  def total_amount(self):
131
138
  if self.type_of_order == Order.OrderType.WALLET.value:
@@ -137,6 +144,7 @@ class Order(DjangoModelCleanMixin, models.Model):
137
144
  )
138
145
  if self.shipping_method:
139
146
  amount += self.shipping_method.shipping_fee
147
+ amount -= self.get_discount_amount
140
148
  return amount
141
149
 
142
150
  @property
@@ -211,6 +219,13 @@ class OrderItem(DjangoModelCleanMixin, models.Model):
211
219
  total_price += attribute_choice.price
212
220
  return total_price
213
221
 
222
+ @property
223
+ def discount_offer_amount(self):
224
+ discount = self.order.discount
225
+ if discount:
226
+ return discount.perc_to_flat(self.total_amount)
227
+ return Decimal(0)
228
+
214
229
  @property
215
230
  def total_amount(self):
216
231
  if self.total_price > 0:
@@ -0,0 +1,116 @@
1
+ import logging
2
+ from datetime import timedelta
3
+
4
+ from django.conf import settings
5
+ from django.core.validators import MaxValueValidator
6
+ from django.db import models
7
+ from django.utils.timezone import now
8
+ from django.utils.translation import gettext_lazy as _
9
+ from django_countries.fields import CountryField
10
+ from ob_dj_otp.core.otp.models import OneTruePairing
11
+
12
+ from ob_dj_store.core.stores.managers import PartnerAuthInfoManager
13
+ from ob_dj_store.core.stores.models._store import Store
14
+ from ob_dj_store.utils.model import DjangoModelCleanMixin
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class Discount(models.Model):
20
+ discount_rate = models.DecimalField(
21
+ max_digits=3, decimal_places=2, validators=[MaxValueValidator(limit_value=1)]
22
+ )
23
+ is_active = models.BooleanField(default=True)
24
+
25
+ # Audit fields
26
+ created_at = models.DateTimeField(auto_now_add=True)
27
+ updated_at = models.DateTimeField(auto_now=True)
28
+
29
+ def __str__(self) -> str:
30
+ return f"{self.discount_rate * 100}% Discount"
31
+
32
+ def perc_to_flat(self, amount):
33
+ return amount * self.discount_rate
34
+
35
+
36
+ def default_offer_end_time():
37
+ return now() + timedelta(days=getattr(settings, "DEFAULT_PARTNER_OFFER_TIME", 60))
38
+
39
+
40
+ class Partner(DjangoModelCleanMixin, models.Model):
41
+ class AuthMethods(models.TextChoices):
42
+ OTP = "OTP", _("One True Pairing")
43
+ CODE = "CODE", _("Promotion code")
44
+
45
+ name = models.CharField(_("Partner's Name"), max_length=255)
46
+ stores = models.ManyToManyField(Store, related_name="partners")
47
+ promotion_code = models.PositiveBigIntegerField(
48
+ _("Promotion code"), null=True, blank=True, unique=True
49
+ )
50
+ auth_method = models.CharField(
51
+ _("Authentication method"), max_length=255, choices=AuthMethods.choices
52
+ )
53
+ country = CountryField(help_text=_("Partner's country."))
54
+ discount = models.ForeignKey(
55
+ Discount, on_delete=models.PROTECT, related_name="partners"
56
+ )
57
+ offer_start_time = models.DateTimeField(_("Offer start date"), default=now)
58
+ offer_end_time = models.DateTimeField(
59
+ _("Offer end date"), default=default_offer_end_time
60
+ )
61
+
62
+ # Audit fields
63
+ created_at = models.DateTimeField(auto_now_add=True)
64
+ updated_at = models.DateTimeField(auto_now=True)
65
+
66
+ def __str__(self) -> str:
67
+ return f"{self.name}"
68
+
69
+
70
+ class PartnerEmailDomain(DjangoModelCleanMixin, models.Model):
71
+ partner = models.ForeignKey(
72
+ Partner, on_delete=models.CASCADE, related_name="domains"
73
+ )
74
+ email_domain = models.CharField(max_length=255, unique=True)
75
+
76
+ # Audit fields
77
+ created_at = models.DateTimeField(auto_now_add=True)
78
+ updated_at = models.DateTimeField(auto_now=True)
79
+
80
+
81
+ class PartnerAuthInfo(models.Model):
82
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
83
+ email = models.EmailField(_("Partner's Email"))
84
+ partner = models.ForeignKey(Partner, on_delete=models.CASCADE)
85
+ authentication_details = models.JSONField(null=True, blank=True)
86
+ authentication_expires = models.DateTimeField()
87
+
88
+ # Audit fields
89
+ created_at = models.DateTimeField(auto_now_add=True)
90
+ updated_at = models.DateTimeField(_("Last Authentifacation"), auto_now=True)
91
+
92
+ objects = PartnerAuthInfoManager()
93
+
94
+ def save(self, **kwargs) -> None:
95
+ self.authentication_expires = now() + timedelta(
96
+ days=getattr(settings, "PARTNER_AUTH_TIME", 365)
97
+ )
98
+ return super().save(**kwargs)
99
+
100
+
101
+ class PartnerOTPAuth(models.Model):
102
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
103
+ otp = models.OneToOneField(
104
+ OneTruePairing, on_delete=models.PROTECT, related_name="partner_otp_auth"
105
+ )
106
+ partner = models.ForeignKey(
107
+ Partner, on_delete=models.CASCADE, related_name="otp_auths"
108
+ )
109
+ email = models.EmailField(_("Partner's Email"))
110
+
111
+ # Audit fields
112
+ created_at = models.DateTimeField(auto_now_add=True)
113
+ updated_at = models.DateTimeField(auto_now=True)
114
+
115
+ def __str__(self) -> str:
116
+ return f"PartnerOTPAuth(PK={self.pk})"
@@ -161,6 +161,7 @@ class WalletTransaction(models.Model):
161
161
  decimal_places=settings.DEFAULT_DECIMAL_PLACES,
162
162
  )
163
163
 
164
+ is_by_admin = models.BooleanField(default=False)
164
165
  is_cashback = models.BooleanField(default=False)
165
166
  is_refund = models.BooleanField(default=False)
166
167
 
@@ -1,9 +1,13 @@
1
1
  import math
2
+ from datetime import timedelta
2
3
 
3
4
  import pycountry
4
5
  from django.core.exceptions import ValidationError
6
+ from django.utils.timezone import now
5
7
  from django.utils.translation import gettext_lazy as _
6
8
 
9
+ from config import settings
10
+
7
11
 
8
12
  def get_data_dict(instance):
9
13
  """
@@ -53,10 +57,13 @@ def distance(origin, destination):
53
57
  return d
54
58
 
55
59
 
56
- def get_currency_by_country(country):
57
- country = pycountry.countries.get(name=country)
58
- currency = pycountry.currencies.get(numeric=country.numeric)
59
- return currency.alpha_3
60
+ def get_currency_by_country(country_value):
61
+ try:
62
+ country = pycountry.countries.get(alpha_2=country_value)
63
+ currency = pycountry.currencies.get(numeric=country.numeric)
64
+ return currency.alpha_3
65
+ except Exception as e:
66
+ return None
60
67
 
61
68
 
62
69
  def get_country_by_currency(currency):
@@ -71,3 +78,23 @@ def validate_currency(value):
71
78
  _("%(value)s is not a currency"),
72
79
  params={"value": value},
73
80
  )
81
+
82
+
83
+ class PartnerAuth:
84
+ def __init__(self, email: str):
85
+ from ob_dj_store.core.stores.models import PartnerAuthInfo, PartnerEmailDomain
86
+
87
+ try:
88
+ domain = PartnerEmailDomain.objects.get(email_domain=email.split("@")[1])
89
+ self.partner = domain.partner
90
+ except PartnerEmailDomain.DoesNotExist:
91
+ raise ValidationError(_("Domain doesn't exist"))
92
+
93
+ # verify email availability
94
+ renew_time = now() + timedelta(
95
+ hours=getattr(settings, "PARTNER_RENEW_AUTH_TIME", 24)
96
+ )
97
+ if PartnerAuthInfo.objects.filter(
98
+ email=email, authentication_expires__gt=renew_time
99
+ ).exists():
100
+ raise ValidationError(_("This email is already in use"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ob-dj-store
3
- Version: 0.0.15.18
3
+ Version: 0.0.16.1
4
4
  Summary: OBytes django application for managing ecommerce stores.
5
5
  Home-page: https://www.obytes.com/
6
6
  Author: OBytes
@@ -1,22 +1,22 @@
1
1
  ob_dj_store/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  ob_dj_store/apis/stores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ob_dj_store/apis/stores/filters.py,sha256=7tp_KUaiTWkFQaFQnePU4eXrOMhbY7oN6SRjr90czzM,8672
4
- ob_dj_store/apis/stores/urls.py,sha256=P4d0iamg2R5lTwc5DTIuhLzJTMSH4f5QpHzGR1_PWE0,1676
5
- ob_dj_store/apis/stores/views.py,sha256=v-WZCpvY3Y9N3rmC6bG3R2NrhikFUDLey5KzaShwgs4,38160
6
- ob_dj_store/apis/stores/rest/serializers/serializers.py,sha256=dGteCoIqCDcrOVGsdjseJgIB1w5EP6q4XJvgUD8fhBQ,49772
3
+ ob_dj_store/apis/stores/filters.py,sha256=cWEIriaBil5iJlnTgFPMYJtyaHNcI3x3EoQ5X12lkCI,8927
4
+ ob_dj_store/apis/stores/urls.py,sha256=xL7CUNJDqxWzb8Gt7gYH63rmQ00rfGoTr9duiu9-0v8,1778
5
+ ob_dj_store/apis/stores/views.py,sha256=llLsupgQ-Rq9KqeE0ajNq7Lpj7bIDkvPN7cbhsd3Nn4,40134
6
+ ob_dj_store/apis/stores/rest/serializers/serializers.py,sha256=5nlrfs9l0rfb1Ai7rKcWrfpKAbiGF_xI3R8Bk2MB-Go,55919
7
7
  ob_dj_store/apis/tap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  ob_dj_store/apis/tap/serializers.py,sha256=KPrBK4h2-fWvEVf6vOj2ww5-USV9WqpyYicIqoHIiXI,1065
9
9
  ob_dj_store/apis/tap/urls.py,sha256=bnOTv6an11kxpo_FdqlhsizlGPLVpNxBjCyKcf3_C9M,367
10
10
  ob_dj_store/apis/tap/views.py,sha256=VnVquybTHlJquxsC0RNTy20dtLXalchO0SlGjSDaBng,2666
11
11
  ob_dj_store/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ob_dj_store/core/stores/__init__.py,sha256=-izNGrxNn_nn3IQXd5pkuES9lSF-AHYb14yhNPozYCI,65
13
- ob_dj_store/core/stores/admin.py,sha256=jMYG6vQ2BZJFT_4uqPoAwAJDAonbPLurz8mbsPpF3gA,10522
14
- ob_dj_store/core/stores/admin_inlines.py,sha256=K7T5IqTqkCKzt_8vqAg3OqOuREqih_lujf1ULB1YFS8,2791
13
+ ob_dj_store/core/stores/admin.py,sha256=klnZPKa5BR0yBqTKAYlJJys5RqpG9V4bLEXNI34h8mI,11680
14
+ ob_dj_store/core/stores/admin_inlines.py,sha256=NM8Ab7htloQdihRBmew4Ie-ENsKhMlKRIsIH06xO1Mw,2903
15
15
  ob_dj_store/core/stores/apps.py,sha256=ZadmEER_dNcQTH617b3fAsYZJSyRw0g46Kjp4eOAsOU,498
16
- ob_dj_store/core/stores/managers.py,sha256=NWYR4x3bCnmkrS0J5vtfpH9Q-VEWSSmvMuGM9xYIlSg,8699
16
+ ob_dj_store/core/stores/managers.py,sha256=DeO8wlN7YvbVZvKZWt5Z4aDgcip54vCioCZH-IRQhuY,9548
17
17
  ob_dj_store/core/stores/receivers.py,sha256=DljYC97C_e1mHduKw9Un6YQmxIdwSIter7yVVZwggFA,3768
18
18
  ob_dj_store/core/stores/settings_validation.py,sha256=eTkRaI6CG5OEJQyI5CF-cNAcvjzXf3GwX5sR97O3v98,3977
19
- ob_dj_store/core/stores/utils.py,sha256=_FwZEIwKdfj3CuYHCz3wKqq5TBb8xak7UiiCB1oggKc,1850
19
+ ob_dj_store/core/stores/utils.py,sha256=70cE4mf64KiEdh5yYhUfLIqykH2MbnN9xlvNgEx5Mk0,2775
20
20
  ob_dj_store/core/stores/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  ob_dj_store/core/stores/gateway/tap/__init__.py,sha256=5Z6azpb6tmr1nRvKwQWzlYw9ruvw-9ZMBWRqEngDKTM,40
22
22
  ob_dj_store/core/stores/gateway/tap/admin.py,sha256=3KgawxLjvpMsYXl_hx1DlKYpsMd-ojSA4FNazVA4CIk,1183
@@ -120,23 +120,28 @@ ob_dj_store/core/stores/migrations/0085_auto_20230726_1528.py,sha256=inhGNw0kttc
120
120
  ob_dj_store/core/stores/migrations/0086_wallet_is_active.py,sha256=wS5H-i1BvbzpuqhtfUZ08CPGAH5X0W8Z8oEnPJ87RCQ,388
121
121
  ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py,sha256=ESHEN8RnSQh_WzsfVhwqdPO5pnSwtRhtWRT2gsUB5ZY,1785
122
122
  ob_dj_store/core/stores/migrations/0088_alter_paymentmethod_payment_provider.py,sha256=SuZ2YFSPaZB2O54jP7U31NH2JD5tKmyR0kalLIuAvQ8,932
123
+ ob_dj_store/core/stores/migrations/0089_discount_partner_partnerauthinfo_partneremaildomain_partnerotpauth.py,sha256=c8IbAcCLkxw0ttI0UQxWkY0ZyBOD_AUDRn6dSXNshLA,7374
124
+ ob_dj_store/core/stores/migrations/0090_auto_20231006_1356.py,sha256=wJGLZDhri35X_Tr72gYrzZP6EEuz3qiKRN9BDsmLfCU,871
125
+ ob_dj_store/core/stores/migrations/0091_auto_20231010_1740.py,sha256=qUjN-qPM3k19dLpG42tEPePacFTTWvussGtsgetBNEU,862
126
+ ob_dj_store/core/stores/migrations/0092_auto_20231012_1339.py,sha256=KzINBlngxNmU-dQjV5VgZssVL6jihxY7jbMfpfDN5K4,1367
123
127
  ob_dj_store/core/stores/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
- ob_dj_store/core/stores/models/__init__.py,sha256=gQGy9jDRGxMH_T6Bc8_utYbx-G8BNAG4SqqghkXOcPc,1741
128
+ ob_dj_store/core/stores/models/__init__.py,sha256=vG1vVOXne3Gg938-015MGrXQAaxqVLYF0Amd85oKrhY,1991
125
129
  ob_dj_store/core/stores/models/_address.py,sha256=qS5TQ9Z12zx_4CrrHvG8PoYVkdiOq_MtbKR14WKh3Hw,1661
126
- ob_dj_store/core/stores/models/_cart.py,sha256=SmlOczSZ2UnEIFcCR107-pcCMlkOL0oWb22h6--x_1k,4779
127
- ob_dj_store/core/stores/models/_favorite.py,sha256=J29ECDsotIgduPpmUmieYTndymReWHkfdsZyQzz6A4g,3042
130
+ ob_dj_store/core/stores/models/_cart.py,sha256=9JYfu-T0_AZiZEY8vaHJ719L4zqVOgvMXj3sj-dixtE,5856
131
+ ob_dj_store/core/stores/models/_favorite.py,sha256=ZVFCsDWB8fOcx5LkhNphTNRGuEUdsHM0ex1-OtFa4gA,3232
128
132
  ob_dj_store/core/stores/models/_feedback.py,sha256=eCUVgprNK5hSRKOS4M_pdR7QH2-rqhoYevlpykhCOLg,1472
129
133
  ob_dj_store/core/stores/models/_inventory.py,sha256=ZU8xDMQZxLnFehkBEGWr-os4AF1IlCn5XnBxvRq9IAs,4314
130
- ob_dj_store/core/stores/models/_order.py,sha256=VHBp3LMveedHNbS7vxP7PE_H_g9t8qgsi7_szQIlBB8,8650
134
+ ob_dj_store/core/stores/models/_order.py,sha256=am-tpHs0b4mu5xdj5xaK3LQjhGs88aKNyhLFX8g8XA4,9139
135
+ ob_dj_store/core/stores/models/_partner.py,sha256=N4nc7FUWBkkkBar6__nJ2Jg1SxOBnt1YCnsLaKpa_vk,4057
131
136
  ob_dj_store/core/stores/models/_payment.py,sha256=mz54fuxmcNQ1pVBC959f7gnbIld1haxIYK0Xuv6UVoM,6320
132
137
  ob_dj_store/core/stores/models/_product.py,sha256=jWbhk-oXngLdHqBTnHNokzN_MhcBArsD1R7UjOnfAzY,15287
133
138
  ob_dj_store/core/stores/models/_store.py,sha256=NnyXG7_L_UYrNKaQYuHOBEWc6DQqAPd1OZJg0xOccKk,8005
134
- ob_dj_store/core/stores/models/_wallet.py,sha256=8wi-H0HMFOdDPc1LbKkQBMGAymiLvBWq2djnN_BicRI,5327
139
+ ob_dj_store/core/stores/models/_wallet.py,sha256=0Ap6CYGFC0IJ9nfgPU1ypDq3r9HQjI5L9J8vz6FE7Es,5380
135
140
  ob_dj_store/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
141
  ob_dj_store/utils/helpers.py,sha256=o7wgypM7mI2vZqZKkhxnTcnHJC8GMQDOuYMnRwXr6tY,2058
137
142
  ob_dj_store/utils/model.py,sha256=DV7hOhTaZL3gh9sptts2jTUFlTArKG3i7oPioq9HLFE,303
138
143
  ob_dj_store/utils/utils.py,sha256=8UVAFB56qUSjJJ5f9vnermtw638gdFy4CFRCuMbns_M,1342
139
- ob_dj_store-0.0.15.18.dist-info/METADATA,sha256=_TNqQ5SliuR9Wz_0YVpKFKPu4V9ziwbLaTnwWyvCIfk,2828
140
- ob_dj_store-0.0.15.18.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
141
- ob_dj_store-0.0.15.18.dist-info/top_level.txt,sha256=CZG3G0ptTkzGnc0dFYN-ZD7YKdJBmm47bsmGwofD_lk,12
142
- ob_dj_store-0.0.15.18.dist-info/RECORD,,
144
+ ob_dj_store-0.0.16.1.dist-info/METADATA,sha256=0NDzIbqnGaB5C25MeBZobX2rD72RhbgXpI_UGy6y3OI,2827
145
+ ob_dj_store-0.0.16.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
146
+ ob_dj_store-0.0.16.1.dist-info/top_level.txt,sha256=CZG3G0ptTkzGnc0dFYN-ZD7YKdJBmm47bsmGwofD_lk,12
147
+ ob_dj_store-0.0.16.1.dist-info/RECORD,,