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.
- ob_dj_store/apis/stores/filters.py +7 -1
- ob_dj_store/apis/stores/rest/serializers/serializers.py +158 -1
- ob_dj_store/apis/stores/urls.py +2 -0
- ob_dj_store/apis/stores/views.py +69 -0
- ob_dj_store/core/stores/admin.py +39 -0
- ob_dj_store/core/stores/admin_inlines.py +5 -0
- ob_dj_store/core/stores/managers.py +21 -1
- ob_dj_store/core/stores/migrations/0089_discount_partner_partnerauthinfo_partneremaildomain_partnerotpauth.py +208 -0
- ob_dj_store/core/stores/migrations/0090_auto_20231006_1356.py +32 -0
- ob_dj_store/core/stores/migrations/0091_auto_20231010_1740.py +30 -0
- ob_dj_store/core/stores/migrations/0092_auto_20231012_1339.py +47 -0
- ob_dj_store/core/stores/models/__init__.py +12 -0
- ob_dj_store/core/stores/models/_cart.py +31 -0
- ob_dj_store/core/stores/models/_favorite.py +6 -2
- ob_dj_store/core/stores/models/_order.py +15 -0
- ob_dj_store/core/stores/models/_partner.py +116 -0
- ob_dj_store/core/stores/models/_wallet.py +1 -0
- ob_dj_store/core/stores/utils.py +31 -4
- {ob_dj_store-0.0.15.18.dist-info → ob_dj_store-0.0.16.1.dist-info}/METADATA +1 -1
- {ob_dj_store-0.0.15.18.dist-info → ob_dj_store-0.0.16.1.dist-info}/RECORD +22 -17
- {ob_dj_store-0.0.15.18.dist-info → ob_dj_store-0.0.16.1.dist-info}/WHEEL +0 -0
- {ob_dj_store-0.0.15.18.dist-info → ob_dj_store-0.0.16.1.dist-info}/top_level.txt +0 -0
@@ -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
|
ob_dj_store/apis/stores/urls.py
CHANGED
@@ -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)),
|
ob_dj_store/apis/stores/views.py
CHANGED
@@ -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
|
+
)
|
ob_dj_store/core/stores/admin.py
CHANGED
@@ -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
|
|
ob_dj_store/core/stores/utils.py
CHANGED
@@ -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(
|
57
|
-
|
58
|
-
|
59
|
-
|
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,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=
|
4
|
-
ob_dj_store/apis/stores/urls.py,sha256=
|
5
|
-
ob_dj_store/apis/stores/views.py,sha256=
|
6
|
-
ob_dj_store/apis/stores/rest/serializers/serializers.py,sha256=
|
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=
|
14
|
-
ob_dj_store/core/stores/admin_inlines.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
127
|
-
ob_dj_store/core/stores/models/_favorite.py,sha256=
|
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=
|
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=
|
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.
|
140
|
-
ob_dj_store-0.0.
|
141
|
-
ob_dj_store-0.0.
|
142
|
-
ob_dj_store-0.0.
|
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,,
|
File without changes
|
File without changes
|