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.
- ob_dj_store/apis/stores/filters.py +42 -19
- ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
- ob_dj_store/apis/stores/urls.py +6 -0
- ob_dj_store/apis/stores/views.py +140 -227
- ob_dj_store/apis/stripe/__init__.py +0 -0
- ob_dj_store/apis/stripe/serializers.py +185 -0
- ob_dj_store/apis/stripe/urls.py +25 -0
- ob_dj_store/apis/stripe/views.py +191 -0
- ob_dj_store/apis/tap/views.py +2 -6
- ob_dj_store/core/stores/admin.py +41 -38
- ob_dj_store/core/stores/admin_inlines.py +8 -13
- ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
- ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
- ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
- ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
- ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
- ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
- ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
- ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
- ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
- ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
- ob_dj_store/core/stores/gateway/tap/models.py +4 -13
- ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
- ob_dj_store/core/stores/managers.py +12 -4
- ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
- ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
- ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
- ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
- ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
- ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
- ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
- ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
- ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
- ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
- ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
- ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
- ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
- ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
- ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
- ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
- ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
- ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
- ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
- ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
- ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
- ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
- ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
- ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
- ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
- ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
- ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
- ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
- ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
- ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
- ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
- ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
- ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
- ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
- ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
- ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
- ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
- ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
- ob_dj_store/core/stores/models/__init__.py +9 -1
- ob_dj_store/core/stores/models/_address.py +1 -3
- ob_dj_store/core/stores/models/_cart.py +11 -5
- ob_dj_store/core/stores/models/_feedback.py +1 -3
- ob_dj_store/core/stores/models/_inventory.py +3 -2
- ob_dj_store/core/stores/models/_order.py +69 -20
- ob_dj_store/core/stores/models/_payment.py +28 -24
- ob_dj_store/core/stores/models/_product.py +31 -17
- ob_dj_store/core/stores/models/_store.py +9 -13
- ob_dj_store/core/stores/models/_wallet.py +34 -26
- ob_dj_store/core/stores/receivers.py +43 -27
- ob_dj_store/core/stores/utils.py +1 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
|
|
5
|
+
from ob_dj_store.core.stores.gateway.stripe.models import StripeCustomer, StripePayment
|
|
6
|
+
from ob_dj_store.core.stores.gateway.stripe.utils import StripeException
|
|
7
|
+
from ob_dj_store.core.stores.models import Payment, PaymentMethod
|
|
8
|
+
|
|
9
|
+
User = get_user_model()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StripeCustomerSerializer(serializers.ModelSerializer):
|
|
13
|
+
"""Serializer for Stripe customer information"""
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
model = StripeCustomer
|
|
17
|
+
fields = [
|
|
18
|
+
"stripe_customer_id",
|
|
19
|
+
"first_name",
|
|
20
|
+
"last_name",
|
|
21
|
+
"email",
|
|
22
|
+
"phone_number",
|
|
23
|
+
"created_at",
|
|
24
|
+
]
|
|
25
|
+
read_only_fields = ["stripe_customer_id", "created_at"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StripePaymentSerializer(serializers.ModelSerializer):
|
|
29
|
+
"""Serializer for Stripe payment information"""
|
|
30
|
+
|
|
31
|
+
amount = serializers.DecimalField(max_digits=10, decimal_places=3, read_only=True)
|
|
32
|
+
currency = serializers.CharField(read_only=True)
|
|
33
|
+
payment_url = serializers.CharField(read_only=True)
|
|
34
|
+
|
|
35
|
+
class Meta:
|
|
36
|
+
model = StripePayment
|
|
37
|
+
fields = [
|
|
38
|
+
"payment_intent_id",
|
|
39
|
+
"client_secret",
|
|
40
|
+
"status",
|
|
41
|
+
"source",
|
|
42
|
+
"amount",
|
|
43
|
+
"currency",
|
|
44
|
+
"payment_url",
|
|
45
|
+
"created_at",
|
|
46
|
+
"updated_at",
|
|
47
|
+
]
|
|
48
|
+
read_only_fields = [
|
|
49
|
+
"payment_intent_id",
|
|
50
|
+
"client_secret",
|
|
51
|
+
"status",
|
|
52
|
+
"source",
|
|
53
|
+
"amount",
|
|
54
|
+
"currency",
|
|
55
|
+
"payment_url",
|
|
56
|
+
"created_at",
|
|
57
|
+
"updated_at",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CreateStripePaymentSerializer(serializers.Serializer):
|
|
62
|
+
"""Serializer for creating Stripe payments"""
|
|
63
|
+
|
|
64
|
+
payment_id = serializers.IntegerField(
|
|
65
|
+
help_text="ID of the Payment object to process with Stripe"
|
|
66
|
+
)
|
|
67
|
+
return_url = serializers.URLField(
|
|
68
|
+
required=False,
|
|
69
|
+
help_text="URL to redirect to after payment completion (optional)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def validate_payment_id(self, value):
|
|
73
|
+
"""Validate that the payment exists and belongs to the user"""
|
|
74
|
+
user = self.context["request"].user
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
payment = Payment.objects.get(id=value, user=user)
|
|
78
|
+
except Payment.DoesNotExist:
|
|
79
|
+
raise serializers.ValidationError(
|
|
80
|
+
_("Payment not found or does not belong to you.")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Check if payment method is Stripe
|
|
84
|
+
if payment.method.payment_provider != "stripe":
|
|
85
|
+
raise serializers.ValidationError(_("Payment method must be Stripe."))
|
|
86
|
+
|
|
87
|
+
# Check if payment is already processed
|
|
88
|
+
if hasattr(payment, "stripe_payment"):
|
|
89
|
+
if payment.stripe_payment.status in ["succeeded", "canceled"]:
|
|
90
|
+
raise serializers.ValidationError(
|
|
91
|
+
_("Payment has already been processed.")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
def create(self, validated_data):
|
|
97
|
+
"""Create a Stripe PaymentIntent for the payment"""
|
|
98
|
+
user = self.context["request"].user
|
|
99
|
+
payment_id = validated_data["payment_id"]
|
|
100
|
+
return_url = validated_data.get("return_url")
|
|
101
|
+
|
|
102
|
+
payment = Payment.objects.get(id=payment_id, user=user)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Create or get existing Stripe payment
|
|
106
|
+
if hasattr(payment, "stripe_payment"):
|
|
107
|
+
stripe_payment = payment.stripe_payment
|
|
108
|
+
else:
|
|
109
|
+
stripe_payment = StripePayment.objects.create(
|
|
110
|
+
payment=payment, user=user
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"payment_intent_id": stripe_payment.payment_intent_id,
|
|
115
|
+
"client_secret": stripe_payment.client_secret,
|
|
116
|
+
"status": stripe_payment.status,
|
|
117
|
+
"amount": stripe_payment.amount,
|
|
118
|
+
"currency": stripe_payment.currency,
|
|
119
|
+
"return_url": return_url,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
except StripeException as e:
|
|
123
|
+
raise serializers.ValidationError({"stripe": str(e)})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class StripePaymentStatusSerializer(serializers.Serializer):
|
|
127
|
+
"""Serializer for checking Stripe payment status"""
|
|
128
|
+
|
|
129
|
+
payment_intent_id = serializers.CharField(
|
|
130
|
+
help_text="Stripe PaymentIntent ID to check status for"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def validate_payment_intent_id(self, value):
|
|
134
|
+
"""Validate that the payment intent exists and belongs to the user"""
|
|
135
|
+
user = self.context["request"].user
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
stripe_payment = StripePayment.objects.get(
|
|
139
|
+
payment_intent_id=value, user=user
|
|
140
|
+
)
|
|
141
|
+
except StripePayment.DoesNotExist:
|
|
142
|
+
raise serializers.ValidationError(
|
|
143
|
+
_("Payment intent not found or does not belong to you.")
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class StripeWebhookEventSerializer(serializers.Serializer):
|
|
150
|
+
"""Serializer for processing Stripe webhook events"""
|
|
151
|
+
|
|
152
|
+
id = serializers.CharField()
|
|
153
|
+
type = serializers.CharField()
|
|
154
|
+
data = serializers.DictField()
|
|
155
|
+
|
|
156
|
+
def validate(self, attrs):
|
|
157
|
+
"""Validate webhook event structure"""
|
|
158
|
+
if "object" not in attrs.get("data", {}):
|
|
159
|
+
raise serializers.ValidationError(_("Invalid webhook event structure."))
|
|
160
|
+
|
|
161
|
+
return attrs
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PaymentMethodSerializer(serializers.ModelSerializer):
|
|
165
|
+
"""Serializer for payment methods with Stripe support"""
|
|
166
|
+
|
|
167
|
+
supports_stripe = serializers.SerializerMethodField()
|
|
168
|
+
|
|
169
|
+
class Meta:
|
|
170
|
+
model = PaymentMethod
|
|
171
|
+
fields = [
|
|
172
|
+
"id",
|
|
173
|
+
"name",
|
|
174
|
+
"name_arabic",
|
|
175
|
+
"description",
|
|
176
|
+
"description_arabic",
|
|
177
|
+
"payment_provider",
|
|
178
|
+
"is_active",
|
|
179
|
+
"supports_stripe",
|
|
180
|
+
]
|
|
181
|
+
read_only_fields = ["supports_stripe"]
|
|
182
|
+
|
|
183
|
+
def get_supports_stripe(self, obj):
|
|
184
|
+
"""Check if this payment method supports Stripe"""
|
|
185
|
+
return obj.payment_provider == "stripe"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django.conf.urls import include
|
|
2
|
+
from django.urls import path
|
|
3
|
+
from rest_framework.routers import SimpleRouter
|
|
4
|
+
|
|
5
|
+
from ob_dj_store.apis.stripe.views import StripePaymentViewSet, StripeWebhookViewSet
|
|
6
|
+
|
|
7
|
+
app_name = "stripe_gateway"
|
|
8
|
+
|
|
9
|
+
router = SimpleRouter(trailing_slash=False)
|
|
10
|
+
|
|
11
|
+
# Payment management endpoints
|
|
12
|
+
router.register(r"payments", StripePaymentViewSet, basename="stripe-payment")
|
|
13
|
+
|
|
14
|
+
# Webhook endpoints
|
|
15
|
+
router.register(r"webhook", StripeWebhookViewSet, basename="stripe-webhook")
|
|
16
|
+
|
|
17
|
+
urlpatterns = [
|
|
18
|
+
# Direct webhook URL for easier access from Stripe
|
|
19
|
+
path(
|
|
20
|
+
"callback/",
|
|
21
|
+
StripeWebhookViewSet.as_view({"post": "callback"}),
|
|
22
|
+
name="webhook-callback",
|
|
23
|
+
),
|
|
24
|
+
path("", include(router.urls)),
|
|
25
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.http import HttpResponse, HttpResponseBadRequest
|
|
4
|
+
from django.utils.decorators import method_decorator
|
|
5
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
6
|
+
from drf_yasg import openapi
|
|
7
|
+
from drf_yasg.utils import swagger_auto_schema
|
|
8
|
+
from rest_framework import mixins, permissions, status, viewsets
|
|
9
|
+
from rest_framework.decorators import action
|
|
10
|
+
from rest_framework.response import Response
|
|
11
|
+
|
|
12
|
+
from ob_dj_store.core.stores.gateway.stripe.models import StripePayment
|
|
13
|
+
from ob_dj_store.core.stores.gateway.stripe.utils import (
|
|
14
|
+
handle_stripe_webhook,
|
|
15
|
+
verify_webhook_signature,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .serializers import (
|
|
19
|
+
CreateStripePaymentSerializer,
|
|
20
|
+
StripeCustomerSerializer,
|
|
21
|
+
StripePaymentSerializer,
|
|
22
|
+
StripePaymentStatusSerializer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StripePaymentViewSet(
|
|
29
|
+
mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
|
30
|
+
):
|
|
31
|
+
"""Stripe payment management endpoints"""
|
|
32
|
+
|
|
33
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
34
|
+
serializer_class = StripePaymentSerializer
|
|
35
|
+
|
|
36
|
+
def get_queryset(self):
|
|
37
|
+
"""Filter stripe payments by current user"""
|
|
38
|
+
return StripePayment.objects.filter(user=self.request.user)
|
|
39
|
+
|
|
40
|
+
def get_serializer_class(self):
|
|
41
|
+
"""Return appropriate serializer class based on action"""
|
|
42
|
+
if self.action == "create_payment":
|
|
43
|
+
return CreateStripePaymentSerializer
|
|
44
|
+
elif self.action == "check_status":
|
|
45
|
+
return StripePaymentStatusSerializer
|
|
46
|
+
return self.serializer_class
|
|
47
|
+
|
|
48
|
+
@swagger_auto_schema(
|
|
49
|
+
operation_summary="Create Stripe Payment",
|
|
50
|
+
operation_description="""
|
|
51
|
+
Create a Stripe PaymentIntent for an existing Payment object.
|
|
52
|
+
Returns client_secret for frontend payment confirmation.
|
|
53
|
+
""",
|
|
54
|
+
tags=["Stripe Payment"],
|
|
55
|
+
request_body=CreateStripePaymentSerializer,
|
|
56
|
+
responses={
|
|
57
|
+
201: openapi.Response(
|
|
58
|
+
description="Payment intent created successfully",
|
|
59
|
+
examples={
|
|
60
|
+
"application/json": {
|
|
61
|
+
"payment_intent_id": "pi_1234567890",
|
|
62
|
+
"client_secret": "pi_1234567890_secret_abc123",
|
|
63
|
+
"status": "requires_payment_method",
|
|
64
|
+
"amount": "25.00",
|
|
65
|
+
"currency": "usd",
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
@action(detail=False, methods=["POST"])
|
|
72
|
+
def create_payment(self, request):
|
|
73
|
+
"""Create a Stripe PaymentIntent for a payment"""
|
|
74
|
+
serializer = self.get_serializer(data=request.data)
|
|
75
|
+
serializer.is_valid(raise_exception=True)
|
|
76
|
+
|
|
77
|
+
result = serializer.save()
|
|
78
|
+
return Response(result, status=status.HTTP_201_CREATED)
|
|
79
|
+
|
|
80
|
+
@swagger_auto_schema(
|
|
81
|
+
operation_summary="Check Payment Status",
|
|
82
|
+
operation_description="""
|
|
83
|
+
Check the current status of a Stripe payment.
|
|
84
|
+
""",
|
|
85
|
+
tags=["Stripe Payment"],
|
|
86
|
+
request_body=StripePaymentStatusSerializer,
|
|
87
|
+
)
|
|
88
|
+
@action(detail=False, methods=["POST"])
|
|
89
|
+
def check_status(self, request):
|
|
90
|
+
"""Check the status of a Stripe payment"""
|
|
91
|
+
serializer = self.get_serializer(data=request.data)
|
|
92
|
+
serializer.is_valid(raise_exception=True)
|
|
93
|
+
|
|
94
|
+
payment_intent_id = serializer.validated_data["payment_intent_id"]
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
stripe_payment = StripePayment.objects.get(
|
|
98
|
+
payment_intent_id=payment_intent_id, user=request.user
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
payment_serializer = StripePaymentSerializer(stripe_payment)
|
|
102
|
+
return Response(payment_serializer.data)
|
|
103
|
+
|
|
104
|
+
except StripePayment.DoesNotExist:
|
|
105
|
+
return Response(
|
|
106
|
+
{"error": "Payment not found"}, status=status.HTTP_404_NOT_FOUND
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@swagger_auto_schema(
|
|
110
|
+
operation_summary="Get User's Stripe Customer",
|
|
111
|
+
operation_description="""
|
|
112
|
+
Get or create Stripe customer information for the current user.
|
|
113
|
+
""",
|
|
114
|
+
tags=["Stripe Payment"],
|
|
115
|
+
)
|
|
116
|
+
@action(detail=False, methods=["GET"])
|
|
117
|
+
def customer(self, request):
|
|
118
|
+
"""Get user's Stripe customer information"""
|
|
119
|
+
try:
|
|
120
|
+
from ob_dj_store.core.stores.gateway.stripe.utils import (
|
|
121
|
+
get_or_create_stripe_customer,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
stripe_customer = get_or_create_stripe_customer(request.user)
|
|
125
|
+
serializer = StripeCustomerSerializer(stripe_customer)
|
|
126
|
+
return Response(serializer.data)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Error getting Stripe customer: {e}")
|
|
130
|
+
return Response(
|
|
131
|
+
{"error": "Failed to get customer information"},
|
|
132
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@method_decorator(csrf_exempt, name="dispatch")
|
|
137
|
+
class StripeWebhookViewSet(viewsets.GenericViewSet):
|
|
138
|
+
"""Stripe webhook endpoints"""
|
|
139
|
+
|
|
140
|
+
permission_classes = [permissions.AllowAny]
|
|
141
|
+
|
|
142
|
+
@swagger_auto_schema(
|
|
143
|
+
operation_summary="Stripe Webhook Callback",
|
|
144
|
+
operation_description="""
|
|
145
|
+
Handle Stripe webhook events for payment status updates.
|
|
146
|
+
This endpoint is called by Stripe when payment events occur.
|
|
147
|
+
""",
|
|
148
|
+
tags=["Stripe Webhook"],
|
|
149
|
+
)
|
|
150
|
+
@action(detail=False, methods=["POST"])
|
|
151
|
+
def callback(self, request):
|
|
152
|
+
"""Handle Stripe webhook callbacks"""
|
|
153
|
+
logger.info(
|
|
154
|
+
f"Received webhook request from IP: {request.META.get('REMOTE_ADDR')}"
|
|
155
|
+
)
|
|
156
|
+
logger.info(f"Request headers: {dict(request.META)}")
|
|
157
|
+
|
|
158
|
+
payload = request.body
|
|
159
|
+
sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
|
|
160
|
+
|
|
161
|
+
logger.info(f"Payload length: {len(payload)}")
|
|
162
|
+
logger.info(f"Signature header present: {bool(sig_header)}")
|
|
163
|
+
|
|
164
|
+
if not sig_header:
|
|
165
|
+
logger.warning("Missing Stripe signature header")
|
|
166
|
+
return HttpResponseBadRequest("Missing signature")
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
# Verify webhook signature
|
|
170
|
+
event = verify_webhook_signature(payload, sig_header)
|
|
171
|
+
logger.info(f"Received Stripe webhook: {event['type']}")
|
|
172
|
+
|
|
173
|
+
# Handle the webhook
|
|
174
|
+
result = handle_stripe_webhook(event)
|
|
175
|
+
|
|
176
|
+
if result:
|
|
177
|
+
logger.info("Webhook processed successfully")
|
|
178
|
+
return HttpResponse("OK", status=200)
|
|
179
|
+
else:
|
|
180
|
+
logger.error("Failed to process webhook")
|
|
181
|
+
return HttpResponseBadRequest("Failed to process webhook")
|
|
182
|
+
|
|
183
|
+
except ValueError as e:
|
|
184
|
+
logger.error(f"Invalid payload: {e}")
|
|
185
|
+
return HttpResponseBadRequest("Invalid payload")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Webhook error: {e}")
|
|
188
|
+
import traceback
|
|
189
|
+
|
|
190
|
+
logger.error(f"Webhook error traceback: {traceback.format_exc()}")
|
|
191
|
+
return HttpResponseBadRequest(str(e))
|
ob_dj_store/apis/tap/views.py
CHANGED
|
@@ -28,9 +28,7 @@ class TapPaymentViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
|
28
28
|
operation_description="""
|
|
29
29
|
gateway_tap callback
|
|
30
30
|
""",
|
|
31
|
-
tags=[
|
|
32
|
-
"TAP Payment",
|
|
33
|
-
],
|
|
31
|
+
tags=["TAP Payment",],
|
|
34
32
|
)
|
|
35
33
|
@action(detail=False, methods=["POST"], permission_classes=[permissions.AllowAny])
|
|
36
34
|
def callback(self, request) -> typing.Any:
|
|
@@ -55,9 +53,7 @@ class TapPaymentViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
|
55
53
|
operation_description="""
|
|
56
54
|
Retrieve Tap Transaction from charge id
|
|
57
55
|
""",
|
|
58
|
-
tags=[
|
|
59
|
-
"TAP Payment",
|
|
60
|
-
],
|
|
56
|
+
tags=["TAP Payment",],
|
|
61
57
|
)
|
|
62
58
|
@action(detail=False, methods=["get"], permission_classes=[permissions.AllowAny])
|
|
63
59
|
def get(self, request, *args, **kwargs):
|
ob_dj_store/core/stores/admin.py
CHANGED
|
@@ -3,8 +3,8 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from django import forms
|
|
5
5
|
from django.contrib import admin
|
|
6
|
-
from django.db.models import DecimalField,
|
|
7
|
-
from django.db.models.functions import
|
|
6
|
+
from django.db.models import DecimalField, F, Sum, Value
|
|
7
|
+
from django.db.models.functions import Coalesce, ExtractWeek
|
|
8
8
|
from django.utils.translation import gettext_lazy as _
|
|
9
9
|
from import_export.admin import ImportExportModelAdmin
|
|
10
10
|
from leaflet.admin import LeafletGeoAdmin
|
|
@@ -24,6 +24,7 @@ from ob_dj_store.core.stores.admin_inlines import (
|
|
|
24
24
|
ProductAttributeInlineAdmin,
|
|
25
25
|
ProductMediaInlineAdmin,
|
|
26
26
|
ProductVariantInlineAdmin,
|
|
27
|
+
TipAmountInlineAdmin,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
|
|
@@ -84,6 +85,8 @@ class StoreAdmin(LeafletGeoAdmin):
|
|
|
84
85
|
"minimum_order_amount",
|
|
85
86
|
"delivery_charges",
|
|
86
87
|
"min_free_delivery_amount",
|
|
88
|
+
"created_at",
|
|
89
|
+
"updated_at",
|
|
87
90
|
]
|
|
88
91
|
# define the pickup addresses field as a ManyToManyField
|
|
89
92
|
# to the address model
|
|
@@ -110,7 +113,7 @@ class StoreAdmin(LeafletGeoAdmin):
|
|
|
110
113
|
"pickup_addresses",
|
|
111
114
|
"image",
|
|
112
115
|
"currency",
|
|
113
|
-
"
|
|
116
|
+
"timezone",
|
|
114
117
|
)
|
|
115
118
|
},
|
|
116
119
|
),
|
|
@@ -140,16 +143,10 @@ class CategoryAdmin(admin.ModelAdmin):
|
|
|
140
143
|
]
|
|
141
144
|
|
|
142
145
|
def get_queryset(self, request):
|
|
143
|
-
return (
|
|
144
|
-
super()
|
|
145
|
-
.get_queryset(request)
|
|
146
|
-
.prefetch_related(
|
|
147
|
-
"availability_hours",
|
|
148
|
-
)
|
|
149
|
-
)
|
|
146
|
+
return super().get_queryset(request).prefetch_related("availability_hours",)
|
|
150
147
|
|
|
151
148
|
|
|
152
|
-
class ProductVariantAdmin(admin.ModelAdmin):
|
|
149
|
+
class ProductVariantAdmin(ImportExportModelAdmin, admin.ModelAdmin):
|
|
153
150
|
inlines = [
|
|
154
151
|
InventoryInlineAdmin,
|
|
155
152
|
ProductAttributeInlineAdmin,
|
|
@@ -214,13 +211,7 @@ class ProductAdmin(admin.ModelAdmin):
|
|
|
214
211
|
return [category.name for category in obj.category.all()]
|
|
215
212
|
|
|
216
213
|
def get_queryset(self, request):
|
|
217
|
-
return (
|
|
218
|
-
super()
|
|
219
|
-
.get_queryset(request)
|
|
220
|
-
.prefetch_related(
|
|
221
|
-
"category",
|
|
222
|
-
)
|
|
223
|
-
)
|
|
214
|
+
return super().get_queryset(request).prefetch_related("category",)
|
|
224
215
|
|
|
225
216
|
|
|
226
217
|
class ProductAttributeAdmin(admin.ModelAdmin):
|
|
@@ -286,25 +277,19 @@ class CartAdmin(admin.ModelAdmin):
|
|
|
286
277
|
search_fields = [
|
|
287
278
|
"customer__email",
|
|
288
279
|
]
|
|
280
|
+
autocomplete_fields = ["customer"]
|
|
289
281
|
|
|
290
282
|
def get_queryset(self, request):
|
|
291
283
|
queryset = super().get_queryset(request)
|
|
292
284
|
queryset = queryset.annotate(
|
|
293
|
-
calculated_total_price=
|
|
285
|
+
calculated_total_price=Coalesce(
|
|
294
286
|
Sum(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
F("items__product_variant__inventories__price")
|
|
298
|
-
- F("items__product_variant__inventories__price")
|
|
299
|
-
* F("items__product_variant__inventories__discount_percent")
|
|
300
|
-
/ 100
|
|
301
|
-
),
|
|
302
|
-
output_field=DecimalField(),
|
|
303
|
-
)
|
|
304
|
-
* F("items__quantity"),
|
|
305
|
-
default=Value(0),
|
|
287
|
+
F("items__quantity")
|
|
288
|
+
* F("items__product_variant__inventories__price"),
|
|
306
289
|
output_field=DecimalField(),
|
|
307
|
-
)
|
|
290
|
+
),
|
|
291
|
+
Value(0),
|
|
292
|
+
output_field=DecimalField(),
|
|
308
293
|
)
|
|
309
294
|
)
|
|
310
295
|
return queryset
|
|
@@ -312,7 +297,9 @@ class CartAdmin(admin.ModelAdmin):
|
|
|
312
297
|
def calculated_total_price(self, obj):
|
|
313
298
|
return obj.calculated_total_price
|
|
314
299
|
|
|
315
|
-
calculated_total_price.admin_order_field =
|
|
300
|
+
calculated_total_price.admin_order_field = (
|
|
301
|
+
"calculated_total_price" # Allow sorting by total_price
|
|
302
|
+
)
|
|
316
303
|
calculated_total_price.short_description = "Price"
|
|
317
304
|
|
|
318
305
|
|
|
@@ -383,17 +370,13 @@ class OrderAdmin(ImportExportModelAdmin, admin.ModelAdmin):
|
|
|
383
370
|
"status",
|
|
384
371
|
WeekNumberFilter, # Include the filter instance instead of the class name
|
|
385
372
|
]
|
|
373
|
+
autocomplete_fields = ["customer"]
|
|
386
374
|
|
|
387
375
|
def get_queryset(self, request):
|
|
388
376
|
queryset = (
|
|
389
377
|
super()
|
|
390
378
|
.get_queryset(request)
|
|
391
|
-
.select_related(
|
|
392
|
-
"shipping_method",
|
|
393
|
-
"payment_method",
|
|
394
|
-
"store",
|
|
395
|
-
"customer",
|
|
396
|
-
)
|
|
379
|
+
.select_related("shipping_method", "payment_method", "store", "customer",)
|
|
397
380
|
.prefetch_related(
|
|
398
381
|
"items",
|
|
399
382
|
"items__product_variant__inventories",
|
|
@@ -422,6 +405,7 @@ class PaymentAdmin(ImportExportModelAdmin, admin.ModelAdmin):
|
|
|
422
405
|
search_fields = ["orders__store__name", "user__email"]
|
|
423
406
|
readonly_fields = ("orders",)
|
|
424
407
|
date_hierarchy = "created_at"
|
|
408
|
+
autocomplete_fields = ["user"]
|
|
425
409
|
|
|
426
410
|
|
|
427
411
|
class InventoryAdmin(admin.ModelAdmin):
|
|
@@ -532,6 +516,7 @@ class PartnerAdmin(admin.ModelAdmin):
|
|
|
532
516
|
inlines = [
|
|
533
517
|
PartnerEmailDomainInlineAdmin,
|
|
534
518
|
]
|
|
519
|
+
ordering = ["created_at"]
|
|
535
520
|
|
|
536
521
|
|
|
537
522
|
class DiscountAdmin(admin.ModelAdmin):
|
|
@@ -551,6 +536,23 @@ class PartnerAuthInfoAdmin(admin.ModelAdmin):
|
|
|
551
536
|
list_filter = ["authentication_expires", "partner"]
|
|
552
537
|
|
|
553
538
|
|
|
539
|
+
class TipAdmin(admin.ModelAdmin):
|
|
540
|
+
list_display = (
|
|
541
|
+
"id",
|
|
542
|
+
"name",
|
|
543
|
+
"description",
|
|
544
|
+
"is_active",
|
|
545
|
+
"country",
|
|
546
|
+
)
|
|
547
|
+
search_fields = [
|
|
548
|
+
"name",
|
|
549
|
+
]
|
|
550
|
+
list_filter = ("country",)
|
|
551
|
+
inlines = [
|
|
552
|
+
TipAmountInlineAdmin,
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
|
|
554
556
|
admin.site.register(models.Store, StoreAdmin)
|
|
555
557
|
admin.site.register(models.ShippingMethod, ShippingMethodAdmin)
|
|
556
558
|
admin.site.register(models.PaymentMethod, PaymentMethodAdmin)
|
|
@@ -575,3 +577,4 @@ admin.site.register(models.Partner, PartnerAdmin)
|
|
|
575
577
|
admin.site.register(models.Discount, DiscountAdmin)
|
|
576
578
|
admin.site.register(models.PartnerAuthInfo, PartnerAuthInfoAdmin)
|
|
577
579
|
admin.site.register(models.CountryPaymentMethod, CountryPaymentMethodAdmin)
|
|
580
|
+
admin.site.register(models.Tip, TipAdmin)
|
|
@@ -13,6 +13,7 @@ class OpeningHoursInlineAdmin(admin.TabularInline):
|
|
|
13
13
|
"from_hour",
|
|
14
14
|
"to_hour",
|
|
15
15
|
"always_open",
|
|
16
|
+
"is_open_after_midnight",
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
def get_queryset(self, request):
|
|
@@ -26,13 +27,7 @@ class AvailabilityHoursInlineAdmin(admin.TabularInline):
|
|
|
26
27
|
fields = ["store", "weekday", "from_hour", "to_hour"]
|
|
27
28
|
|
|
28
29
|
def get_queryset(self, request):
|
|
29
|
-
return (
|
|
30
|
-
super()
|
|
31
|
-
.get_queryset(request)
|
|
32
|
-
.select_related(
|
|
33
|
-
"store",
|
|
34
|
-
)
|
|
35
|
-
)
|
|
30
|
+
return super().get_queryset(request).select_related("store",)
|
|
36
31
|
|
|
37
32
|
|
|
38
33
|
class PhoneContactInlineAdmin(admin.TabularInline):
|
|
@@ -107,12 +102,7 @@ class CartItemInlineAdmin(admin.TabularInline):
|
|
|
107
102
|
class OrderItemInline(admin.TabularInline):
|
|
108
103
|
model = models.OrderItem
|
|
109
104
|
extra = 0
|
|
110
|
-
fields = (
|
|
111
|
-
"product_variant",
|
|
112
|
-
"quantity",
|
|
113
|
-
"unit_value",
|
|
114
|
-
"total_amount",
|
|
115
|
-
)
|
|
105
|
+
fields = ("product_variant", "quantity", "unit_value", "total_amount", "notes")
|
|
116
106
|
readonly_fields = (
|
|
117
107
|
"unit_value",
|
|
118
108
|
"total_amount",
|
|
@@ -130,3 +120,8 @@ class InventoryOperationInlineAdmin(admin.TabularInline):
|
|
|
130
120
|
class PartnerEmailDomainInlineAdmin(admin.TabularInline):
|
|
131
121
|
model = models.PartnerEmailDomain
|
|
132
122
|
extra = 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TipAmountInlineAdmin(admin.TabularInline):
|
|
126
|
+
model = models.TipAmount
|
|
127
|
+
extra = 0
|