ob-dj-store 0.0.19__py3-none-any.whl → 0.0.23.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. ob_dj_store/apis/stores/filters.py +42 -19
  2. ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
  3. ob_dj_store/apis/stores/urls.py +6 -0
  4. ob_dj_store/apis/stores/views.py +140 -227
  5. ob_dj_store/apis/stripe/__init__.py +0 -0
  6. ob_dj_store/apis/stripe/serializers.py +185 -0
  7. ob_dj_store/apis/stripe/urls.py +25 -0
  8. ob_dj_store/apis/stripe/views.py +191 -0
  9. ob_dj_store/apis/tap/views.py +2 -6
  10. ob_dj_store/core/stores/admin.py +41 -38
  11. ob_dj_store/core/stores/admin_inlines.py +8 -13
  12. ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
  13. ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
  14. ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
  15. ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
  16. ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
  17. ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
  18. ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
  19. ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
  20. ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
  21. ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
  22. ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
  23. ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
  24. ob_dj_store/core/stores/gateway/tap/models.py +4 -13
  25. ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
  26. ob_dj_store/core/stores/managers.py +12 -4
  27. ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
  28. ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
  29. ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
  30. ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
  31. ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
  32. ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
  33. ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
  34. ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
  35. ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
  36. ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
  37. ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
  38. ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
  39. ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
  40. ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
  41. ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
  42. ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
  43. ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
  44. ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
  45. ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
  46. ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
  47. ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
  48. ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
  49. ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
  50. ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
  51. ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
  52. ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
  53. ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
  54. ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
  55. ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
  56. ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
  57. ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
  58. ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
  59. ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
  60. ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
  61. ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
  62. ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
  63. ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
  64. ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
  65. ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
  66. ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
  67. ob_dj_store/core/stores/models/__init__.py +9 -1
  68. ob_dj_store/core/stores/models/_address.py +1 -3
  69. ob_dj_store/core/stores/models/_cart.py +11 -5
  70. ob_dj_store/core/stores/models/_feedback.py +1 -3
  71. ob_dj_store/core/stores/models/_inventory.py +3 -2
  72. ob_dj_store/core/stores/models/_order.py +69 -20
  73. ob_dj_store/core/stores/models/_payment.py +28 -24
  74. ob_dj_store/core/stores/models/_product.py +31 -17
  75. ob_dj_store/core/stores/models/_store.py +9 -13
  76. ob_dj_store/core/stores/models/_wallet.py +34 -26
  77. ob_dj_store/core/stores/receivers.py +43 -27
  78. ob_dj_store/core/stores/utils.py +1 -2
  79. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
  80. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
  81. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
  82. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  import calendar
2
2
  import logging
3
3
  import typing
4
- from datetime import timedelta
4
+ from collections import OrderedDict
5
+ from datetime import datetime, timedelta
5
6
  from decimal import Decimal
6
7
 
7
8
  import pycountry
@@ -51,12 +52,14 @@ from ob_dj_store.core.stores.models import (
51
52
  ShippingMethod,
52
53
  Store,
53
54
  Tax,
55
+ Tip,
56
+ TipAmount,
54
57
  Wallet,
55
58
  WalletMedia,
56
59
  WalletTransaction,
57
60
  )
58
61
  from ob_dj_store.core.stores.models._inventory import Inventory
59
- from ob_dj_store.core.stores.utils import PartnerAuth, distance
62
+ from ob_dj_store.core.stores.utils import PartnerAuth, distance, get_currency_by_country
60
63
 
61
64
  logger = logging.getLogger(__name__)
62
65
 
@@ -283,6 +286,8 @@ class OrderSerializer(serializers.ModelSerializer):
283
286
  "created_at",
284
287
  "updated_at",
285
288
  "store_name",
289
+ "tip_percentage",
290
+ "tip_value",
286
291
  )
287
292
  extra_kwargs = {
288
293
  "customer": {"read_only": True},
@@ -329,8 +334,7 @@ class OrderSerializer(serializers.ModelSerializer):
329
334
  return store
330
335
 
331
336
  def _validate_user_address(
332
- self,
333
- attrs,
337
+ self, attrs,
334
338
  ):
335
339
  if "shipping_address" not in attrs:
336
340
  raise ValidationError(
@@ -362,7 +366,7 @@ class OrderSerializer(serializers.ModelSerializer):
362
366
  raise serializers.ValidationError(errors)
363
367
  email = gift_details.get("email")
364
368
  phone_number = gift_details.get("phone_number")
365
-
369
+ user = self.context["request"].user
366
370
  if email and phone_number:
367
371
  raise serializers.ValidationError(
368
372
  _("Both Email and Phone number cannot be provided.")
@@ -376,7 +380,12 @@ class OrderSerializer(serializers.ModelSerializer):
376
380
  validate_email(email)
377
381
  except ValidationError:
378
382
  raise serializers.ValidationError(_("Invalid Email format."))
383
+ try:
384
+ if not gift_details["currency"]:
385
+ gift_details["currency"] = get_currency_by_country(user.country.code)
379
386
 
387
+ except Exception:
388
+ raise serializers.ValidationError("Currency is required")
380
389
  if not pycountry.currencies.get(alpha_3=gift_details["currency"]):
381
390
  raise serializers.ValidationError("Gift currency is not valid")
382
391
  try:
@@ -396,29 +405,39 @@ class OrderSerializer(serializers.ModelSerializer):
396
405
 
397
406
  def _validate_pickup_time(self, store, pickup_time):
398
407
  # validate that the pickup_time is always in the future
399
- if pickup_time < localtime(now()):
408
+ if pickup_time < localtime(now(), self._get_store().timezone):
400
409
  raise serializers.ValidationError(_("Pickup time must be in the future"))
401
410
  pickup_time_time = pickup_time.time()
402
411
  # validate that the pickup_time is between store's opening hours and closing hours
403
412
  try:
404
413
  op_hour = store.opening_hours.get(weekday=pickup_time.weekday() + 1)
414
+ from_hour = op_hour.from_hour
415
+ to_hour = op_hour.to_hour
405
416
  if store.currency == "AED":
406
- op_hour.to_hour = op_hour.to_hour + timedelta(hours=1)
407
- op_hour.from_hour = op_hour.to_hour + timedelta(hours=1)
408
- op_hour.save()
409
- if store.is_open_after_midnight:
410
- if op_hour.to_hour < pickup_time_time < op_hour.from_hour:
411
- raise serializers.ValidationError(
412
- _("Pickup time must be between store's opening hours")
417
+ try:
418
+ pickup_time_time += timedelta(hours=1)
419
+ except Exception as e:
420
+ logger.info(
421
+ f"The fix for UAE stores timezone failed due to this error {e}"
413
422
  )
414
- elif (
415
- not op_hour.always_open
416
- and pickup_time_time > op_hour.to_hour
417
- or pickup_time_time < op_hour.from_hour
423
+
424
+ if (
425
+ op_hour.is_open_after_midnight
426
+ and not op_hour.always_open
427
+ and (pickup_time_time > to_hour and pickup_time_time < from_hour)
418
428
  ):
419
429
  raise serializers.ValidationError(
420
430
  _("Pickup time must be between store's opening hours")
421
431
  )
432
+ if (
433
+ not op_hour.is_open_after_midnight
434
+ and not op_hour.always_open
435
+ and not from_hour <= pickup_time_time <= to_hour
436
+ ):
437
+ raise serializers.ValidationError(
438
+ _("Pickup time must be between store's opening hours")
439
+ )
440
+
422
441
  except ObjectDoesNotExist:
423
442
  logger.error(f"{store.name} Store doesn't have opening hours for ")
424
443
 
@@ -434,6 +453,7 @@ class OrderSerializer(serializers.ModelSerializer):
434
453
  attrs["extra_infos"] = {}
435
454
  stores = Store.objects.filter(store_items__cart=user.cart).distinct()
436
455
  gift_details = attrs["extra_infos"].get("gift_details", None)
456
+ unavailable_items = []
437
457
  if gift_details:
438
458
  self._validate_digital_product(gift_details, attrs)
439
459
  currency = gift_details["currency"]
@@ -445,7 +465,17 @@ class OrderSerializer(serializers.ModelSerializer):
445
465
  if len(stores) > 1:
446
466
  raise ValidationError(_("You cannot order from different stores"))
447
467
  for item in user.cart.items.all():
448
- if not item.inventory:
468
+ if item.inventory:
469
+ if item.inventory.is_snoozed:
470
+ unavailable_items.append(item)
471
+ elif not item.inventory.is_uncountable:
472
+ if (
473
+ not item.inventory.quantity
474
+ or item.inventory.quantity == 0
475
+ ):
476
+ unavailable_items.append(item)
477
+
478
+ elif not item.inventory:
449
479
  if language and item.product_variant.product.name_arabic:
450
480
  product_name = item.product_variant.product.name_arabic
451
481
  else:
@@ -455,8 +485,14 @@ class OrderSerializer(serializers.ModelSerializer):
455
485
  message = (
456
486
  f"{product_name} {variant_name} doesn't exist on this store"
457
487
  )
488
+ unavailable_items.append(item)
458
489
  raise ValidationError(_(message))
459
-
490
+ if unavailable_items:
491
+ raise ValidationError(
492
+ _(
493
+ "The cart has items that are not available in the selected store"
494
+ )
495
+ )
460
496
  if "shipping_method" in attrs:
461
497
  if (
462
498
  attrs["shipping_method"].type
@@ -496,9 +532,15 @@ class OrderSerializer(serializers.ModelSerializer):
496
532
  return super().validate(attrs)
497
533
 
498
534
  def perform_payment(self, amount, payment_method, order_store, orders, currency):
535
+ from django.conf import settings
536
+
537
+ from ob_dj_store.core.stores.gateway.stripe.utils import StripeException
499
538
  from ob_dj_store.core.stores.gateway.tap.utils import TapException
500
539
 
501
540
  user = self.context["request"].user
541
+ payment_transaction = None
542
+ charge_id = None
543
+
502
544
  try:
503
545
  payment = Payment.objects.create(
504
546
  user=user,
@@ -507,21 +549,40 @@ class OrderSerializer(serializers.ModelSerializer):
507
549
  currency=currency,
508
550
  orders=orders,
509
551
  )
510
- tap_transaction = payment.tap_payment
552
+
553
+ # Handle different payment gateways
554
+ if payment_method and payment_method.payment_provider == settings.STRIPE:
555
+ payment_transaction = payment.stripe_payment
556
+ charge_id = (
557
+ payment_transaction.payment_intent_id
558
+ if payment_transaction
559
+ else None
560
+ )
561
+ elif payment_method and payment_method.payment_provider in [
562
+ settings.TAP_CREDIT_CARD,
563
+ settings.TAP_KNET,
564
+ settings.TAP_ALL,
565
+ settings.MADA,
566
+ settings.BENEFIT,
567
+ ]:
568
+ payment_transaction = payment.tap_payment
569
+ charge_id = (
570
+ payment_transaction.charge_id if payment_transaction else None
571
+ )
572
+
511
573
  except ValidationError as err:
512
574
  raise serializers.ValidationError(detail=err.messages)
513
- except TapException as err:
514
- raise serializers.ValidationError({"tap": _(str(err))})
575
+ except (TapException, StripeException) as err:
576
+ raise serializers.ValidationError({"payment_gateway": _(str(err))})
515
577
  except ObjectDoesNotExist as err:
516
578
  logger.info(
517
579
  f"Payment Object not created: user:{user}, method:{payment_method}, currency:{currency}, error:{err}"
518
580
  )
519
- tap_transaction = None
520
581
 
521
582
  return {
522
583
  "orders": orders,
523
584
  "payment_url": payment.payment_url,
524
- "charge_id": tap_transaction.charge_id if tap_transaction else None,
585
+ "charge_id": charge_id,
525
586
  }
526
587
 
527
588
  def create(self, validated_data: typing.Dict):
@@ -533,7 +594,12 @@ class OrderSerializer(serializers.ModelSerializer):
533
594
  amount = gift_details["price"]
534
595
  order = Order.objects.create(customer=user, **validated_data)
535
596
  orders.append(order)
536
- currency = gift_details["currency"]
597
+ try:
598
+ default_currency = get_currency_by_country(user.country.code)
599
+ except Exception as e:
600
+ default_currency = "KWD"
601
+ logger.info(f"Couldn't fetch currency due to this error {e}")
602
+ currency = gift_details.get("currency", default_currency)
537
603
  else:
538
604
  cart = user.cart
539
605
  stores = Store.objects.filter(store_items__cart=cart).distinct()
@@ -552,7 +618,11 @@ class OrderSerializer(serializers.ModelSerializer):
552
618
  attributes=list(item.attribute_choices.all()),
553
619
  notes=item.notes,
554
620
  )
621
+ if order.tip_percentage:
622
+ order.tip_value = order.calculate_tip()
623
+ order.save()
555
624
  orders.append(order)
625
+
556
626
  amount = Decimal(
557
627
  sum(map(lambda order: Decimal(order.total_amount) or 0, orders))
558
628
  )
@@ -703,9 +773,7 @@ class CartItemSerializer(
703
773
  "is_multi_variant",
704
774
  )
705
775
  extra_kwargs = {
706
- "store": {
707
- "required": True,
708
- },
776
+ "store": {"required": True,},
709
777
  }
710
778
 
711
779
  def get_is_multi_variant(self, obj):
@@ -713,7 +781,10 @@ class CartItemSerializer(
713
781
  return product.product_variants.all().count() > 1
714
782
 
715
783
  def get_is_available_in_store(self, obj):
716
- return True if obj.inventory else False
784
+ if obj.inventory:
785
+ if obj.inventory.quantity:
786
+ return True
787
+ return False
717
788
 
718
789
  def get_product_id(self, obj):
719
790
  return obj.product_variant.product.id
@@ -743,9 +814,9 @@ class CartItemSerializer(
743
814
  favorites = Favorite.objects.favorites_for_object(
744
815
  obj.product_variant.product, user
745
816
  )
746
- customization = [
747
- obj.product_variant,
748
- ] + [attribute_choice for attribute_choice in obj.attribute_choices.all()]
817
+ customization = [obj.product_variant,] + [
818
+ attribute_choice for attribute_choice in obj.attribute_choices.all()
819
+ ]
749
820
  for favorite in favorites:
750
821
  content_objects = [
751
822
  instance.content_object for instance in favorite.extras.all()
@@ -820,10 +891,19 @@ class CartSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
820
891
  # update or create instance items
821
892
  for item in validated_data["items"]:
822
893
  attribute_choices = item.pop("attribute_choices", None)
823
- cart_item = CartItem.objects.create(
894
+ logger.info("cart item :", item)
895
+ cart_item, created = CartItem.objects.get_or_create(
824
896
  cart=instance,
825
- **item,
897
+ pk=item.pop("id", None),
898
+ defaults={
899
+ "cart": instance,
900
+ "product_variant": item.pop("product_variant", None),
901
+ "store": item.pop("store", None),
902
+ "notes": item.pop("notes", None),
903
+ "quantity": item.pop("quantity", None),
904
+ },
826
905
  )
906
+
827
907
  if attribute_choices:
828
908
  cart_item.attribute_choices.set(attribute_choices)
829
909
  cart_item.save()
@@ -899,6 +979,44 @@ class ProductSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeria
899
979
  return data
900
980
 
901
981
 
982
+ class ProductSearchSerializer(
983
+ ArabicFieldsMixin, FavoriteMixin, serializers.ModelSerializer
984
+ ):
985
+ is_snoozed = serializers.SerializerMethodField()
986
+ is_available = serializers.SerializerMethodField()
987
+
988
+ class Meta:
989
+ model = Product
990
+ fields = (
991
+ "id",
992
+ "name",
993
+ "name_arabic",
994
+ "slug",
995
+ "description",
996
+ "description_arabic",
997
+ "is_snoozed",
998
+ "is_available",
999
+ )
1000
+
1001
+ def get_store_id(self):
1002
+ return self.context["view"].kwargs["store_pk"]
1003
+
1004
+ def get_inventory_for_store(self, product, store_id):
1005
+ if store_id:
1006
+ return product.get_inventory(store_id)
1007
+ return None
1008
+
1009
+ def get_is_snoozed(self, obj):
1010
+ store_id = self.get_store_id()
1011
+ inventory = self.get_inventory_for_store(obj, store_id)
1012
+ return obj.is_snoozed(store_id=store_id) if inventory else False
1013
+
1014
+ def get_is_available(self, obj):
1015
+ store_id = self.get_store_id()
1016
+ inventory = self.get_inventory_for_store(obj, store_id)
1017
+ return bool(inventory and (inventory.quantity or inventory.is_uncountable))
1018
+
1019
+
902
1020
  class ProductListSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
903
1021
  product_images = ProductMediaSerializer(many=True, source="images")
904
1022
 
@@ -948,11 +1066,13 @@ class SubCategorySerializer(ArabicFieldsMixin, serializers.ModelSerializer):
948
1066
  def get_is_available(self, obj) -> bool:
949
1067
  store_id = self.context["request"].query_params.get("store", None)
950
1068
  if store_id:
1069
+ local_tz = Store.objects.get(id=store_id).timezone
1070
+ current_time = localtime(now(), local_tz) if local_tz else localtime(now())
951
1071
  for availability_hours in obj.parent.availability_hours.all():
952
1072
  if availability_hours.category == obj.parent:
953
1073
  return (
954
1074
  availability_hours.from_hour
955
- <= localtime(now()).time()
1075
+ <= current_time.time()
956
1076
  <= availability_hours.to_hour
957
1077
  )
958
1078
  return False
@@ -984,11 +1104,13 @@ class CategorySerializer(ArabicFieldsMixin, serializers.ModelSerializer):
984
1104
  def get_is_available(self, obj) -> bool:
985
1105
  store_id = self.context["request"].query_params.get("store", None)
986
1106
  if store_id:
1107
+ local_tz = Store.objects.get(id=store_id).timezone
1108
+ current_time = localtime(now(), local_tz) if local_tz else localtime(now())
987
1109
  for availability_hours in obj.availability_hours.all():
988
1110
  if availability_hours.category == obj:
989
1111
  return (
990
1112
  availability_hours.from_hour
991
- <= localtime(now()).time()
1113
+ <= current_time.time()
992
1114
  <= availability_hours.to_hour
993
1115
  )
994
1116
  return False
@@ -1099,6 +1221,7 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
1099
1221
  shipping_methods = ShippingMethodSerializer(many=True, read_only=True)
1100
1222
  distance = serializers.SerializerMethodField()
1101
1223
  current_day_opening_hours = serializers.SerializerMethodField()
1224
+ timezone = serializers.SerializerMethodField()
1102
1225
 
1103
1226
  class Meta:
1104
1227
  model = Store
@@ -1127,7 +1250,7 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
1127
1250
  "image",
1128
1251
  "busy_mode",
1129
1252
  "name_arabic",
1130
- "is_open_after_midnight",
1253
+ "timezone",
1131
1254
  )
1132
1255
  extra_kwargs = {
1133
1256
  "image": {"read_only": True, "required": False},
@@ -1136,26 +1259,26 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
1136
1259
  def get_is_closed(self, obj):
1137
1260
  if obj.busy_mode:
1138
1261
  return True
1139
-
1140
- current_time = localtime(now())
1262
+ current_time = localtime(now(), obj.timezone)
1141
1263
  current_op_hour = obj.current_opening_hours
1142
1264
  if current_op_hour:
1143
- if obj.is_open_after_midnight:
1144
- return (
1145
- True
1146
- if current_op_hour.to_hour
1147
- < current_time.time()
1148
- < current_op_hour.from_hour
1149
- else False
1150
- )
1265
+ from_hour = current_op_hour.from_hour
1266
+ to_hour = current_op_hour.to_hour
1267
+ if current_time.tzinfo != "Asia/Dubai":
1268
+ if obj.currency == "AED":
1269
+ try:
1270
+ current_time += timedelta(hours=1)
1271
+ except Exception as e:
1272
+ logger.info(
1273
+ f"The fix for UAE stores timezone failed due to this error {e}"
1274
+ )
1275
+
1276
+ if current_op_hour.is_open_after_midnight:
1277
+ return True if to_hour < current_time.time() < from_hour else False
1151
1278
 
1152
1279
  if current_op_hour.always_open:
1153
1280
  return False
1154
- return (
1155
- not current_op_hour.from_hour
1156
- <= current_time.time()
1157
- <= current_op_hour.to_hour
1158
- )
1281
+ return not from_hour <= current_time.time() <= to_hour
1159
1282
  return True
1160
1283
 
1161
1284
  def get_in_range_delivery(self, obj):
@@ -1255,6 +1378,9 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
1255
1378
  op_hour = f"{settings.DEFAULT_OPENING_HOURS[0]['from_hour']} - {settings.DEFAULT_OPENING_HOURS[0]['to_hour']}"
1256
1379
  return op_hour
1257
1380
 
1381
+ def get_timezone(self, obj):
1382
+ return str(obj.timezone)
1383
+
1258
1384
  def to_representation(self, instance):
1259
1385
  return super().to_representation(instance)
1260
1386
 
@@ -1293,10 +1419,15 @@ class TaxSerializer(serializers.ModelSerializer):
1293
1419
 
1294
1420
 
1295
1421
  class StoreListSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
1422
+ timezone = serializers.SerializerMethodField()
1423
+
1296
1424
  class Meta:
1297
1425
  model = Store
1298
1426
  fields = "__all__"
1299
1427
 
1428
+ def get_timezone(self, obj):
1429
+ return str(obj.timezone)
1430
+
1300
1431
 
1301
1432
  class GenericSerializer(serializers.Serializer):
1302
1433
  """
@@ -1392,9 +1523,12 @@ class FavoriteSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
1392
1523
  try:
1393
1524
  extra = obj.extras.get(content_type=content_type)
1394
1525
  inventory = extra.content_object.inventories.get(store=store_id)
1395
- if inventory.is_uncountable:
1396
- return True
1397
- return inventory.quantity > 0
1526
+ if inventory.is_snoozed:
1527
+ return False
1528
+ else:
1529
+ if inventory.is_uncountable:
1530
+ return True
1531
+ return False
1398
1532
  except ObjectDoesNotExist:
1399
1533
  return False
1400
1534
  return None
@@ -1566,6 +1700,7 @@ class WalletTopUpSerializer(serializers.Serializer):
1566
1700
  store_settings.GOOGLE_PAY,
1567
1701
  store_settings.MADA,
1568
1702
  store_settings.BENEFIT,
1703
+ store_settings.STRIPE,
1569
1704
  ]
1570
1705
  ),
1571
1706
  required=True,
@@ -1597,6 +1732,39 @@ class WalletTransactionSerializer(serializers.ModelSerializer):
1597
1732
  ]
1598
1733
 
1599
1734
 
1735
+ class GroupedWalletTransactionListSerializer(serializers.ListSerializer):
1736
+ def to_representation(self, data):
1737
+ result_dict = OrderedDict()
1738
+ for wallettransaction in data:
1739
+ year = wallettransaction.created_at.year
1740
+ month = wallettransaction.created_at.month
1741
+ title = f"{datetime(year, month, 1).strftime('%b')}, {year}"
1742
+
1743
+ if title not in result_dict:
1744
+ result_dict[title] = {"title": title, "data": []}
1745
+
1746
+ result_dict[title]["data"].append(
1747
+ self.child.to_representation(wallettransaction)
1748
+ )
1749
+
1750
+ return list(result_dict.values())
1751
+
1752
+
1753
+ class WalletTransactionListSerializer(serializers.ModelSerializer):
1754
+ class Meta:
1755
+ model = WalletTransaction
1756
+ fields = [
1757
+ "id",
1758
+ "type",
1759
+ "amount",
1760
+ "created_at",
1761
+ "wallet",
1762
+ "is_cashback",
1763
+ "is_refund",
1764
+ ]
1765
+ list_serializer_class = GroupedWalletTransactionListSerializer
1766
+
1767
+
1600
1768
  class ReorderSerializer(serializers.Serializer):
1601
1769
  force_cart = serializers.BooleanField(default=False)
1602
1770
 
@@ -1769,9 +1937,7 @@ class PartnerAuthInfoSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
1769
1937
  status=OneTruePairing.Statuses.init,
1770
1938
  created_at__gte=timeout,
1771
1939
  ).get(
1772
- email=attrs["email"],
1773
- partner_otp_auth__partner=partner,
1774
- user=user,
1940
+ email=attrs["email"], partner_otp_auth__partner=partner, user=user,
1775
1941
  )
1776
1942
  self.context["otp"] = otp
1777
1943
  except ObjectDoesNotExist as e:
@@ -1808,11 +1974,7 @@ class PartnerAuthInfoSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
1808
1974
  otp.status = OneTruePairing.Statuses.used
1809
1975
  otp.save()
1810
1976
  partner_auth_info = PartnerAuthInfo.objects.update_or_create(
1811
- user=user,
1812
- defaults={
1813
- "partner": partner,
1814
- "email": validated_data["email"],
1815
- },
1977
+ user=user, defaults={"partner": partner, "email": validated_data["email"],},
1816
1978
  )
1817
1979
  return partner_auth_info
1818
1980
 
@@ -1836,3 +1998,34 @@ class CountryPaymentMethodSerialzier(serializers.ModelSerializer):
1836
1998
  instance=qs, context=self.context, many=True
1837
1999
  ).data
1838
2000
  return data
2001
+
2002
+
2003
+ class TipSerializer(serializers.ModelSerializer):
2004
+ tip_amounts = serializers.SerializerMethodField()
2005
+
2006
+ class Meta:
2007
+ model = Tip
2008
+ fields = (
2009
+ "id",
2010
+ "name",
2011
+ "description",
2012
+ "is_applied",
2013
+ "country",
2014
+ "is_active",
2015
+ "tip_amounts",
2016
+ )
2017
+
2018
+ def get_tip_amounts(self, obj):
2019
+ from .serializers import TipAmountSerializer # avoid circular import
2020
+
2021
+ tip_amounts = obj.tip_amounts.all()
2022
+ return TipAmountSerializer(tip_amounts, many=True).data
2023
+
2024
+
2025
+ class TipAmountSerializer(serializers.ModelSerializer):
2026
+ class Meta:
2027
+ model = TipAmount
2028
+ fields = (
2029
+ "id",
2030
+ "percentage",
2031
+ )
@@ -18,8 +18,10 @@ from ob_dj_store.apis.stores.views import (
18
18
  ShippingMethodViewSet,
19
19
  StoreView,
20
20
  TaxViewSet,
21
+ TipsViewSet,
21
22
  TransactionsViewSet,
22
23
  VariantView,
24
+ WalletV2ViewSet,
23
25
  WalletViewSet,
24
26
  )
25
27
 
@@ -43,6 +45,7 @@ router.register(r"cart", CartView, basename="cart")
43
45
  router.register(r"cart-item", CartItemView, basename="cart-item")
44
46
  router.register(r"favorite", FavoriteViewSet, basename="favorite")
45
47
  router.register(r"wallet", WalletViewSet, basename="wallet")
48
+ router.register(r"wallet_v2", WalletV2ViewSet, basename="wallet_v2")
46
49
  router.register(r"payment-method", PaymentMethodViewSet, basename="payment-method")
47
50
  router.register(r"order", ReorderViewSet, basename="re-order")
48
51
  router.register(r"partner/auth", PartnerAuthInfoViewSet, basename="auth")
@@ -52,6 +55,9 @@ router.register(
52
55
  CountryPaymentMethodsViewSet,
53
56
  basename="country-payment-methods",
54
57
  )
58
+ router.register(
59
+ r"tip", TipsViewSet, basename="tip",
60
+ )
55
61
 
56
62
  urlpatterns = [
57
63
  path(r"", include(router.urls)),