ob-dj-store 0.0.21.4__py3-none-any.whl → 0.0.21.6__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/rest/serializers/serializers.py +31 -5
- 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/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/models/_payment.py +4 -0
- ob_dj_store/core/stores/models/_product.py +8 -2
- {ob_dj_store-0.0.21.4.dist-info → ob_dj_store-0.0.21.6.dist-info}/METADATA +1 -1
- {ob_dj_store-0.0.21.4.dist-info → ob_dj_store-0.0.21.6.dist-info}/RECORD +19 -7
- {ob_dj_store-0.0.21.4.dist-info → ob_dj_store-0.0.21.6.dist-info}/WHEEL +0 -0
- {ob_dj_store-0.0.21.4.dist-info → ob_dj_store-0.0.21.6.dist-info}/top_level.txt +0 -0
@@ -529,9 +529,15 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
529
529
|
return super().validate(attrs)
|
530
530
|
|
531
531
|
def perform_payment(self, amount, payment_method, order_store, orders, currency):
|
532
|
+
from django.conf import settings
|
533
|
+
|
534
|
+
from ob_dj_store.core.stores.gateway.stripe.utils import StripeException
|
532
535
|
from ob_dj_store.core.stores.gateway.tap.utils import TapException
|
533
536
|
|
534
537
|
user = self.context["request"].user
|
538
|
+
payment_transaction = None
|
539
|
+
charge_id = None
|
540
|
+
|
535
541
|
try:
|
536
542
|
payment = Payment.objects.create(
|
537
543
|
user=user,
|
@@ -540,21 +546,40 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
540
546
|
currency=currency,
|
541
547
|
orders=orders,
|
542
548
|
)
|
543
|
-
|
549
|
+
|
550
|
+
# Handle different payment gateways
|
551
|
+
if payment_method and payment_method.payment_provider == settings.STRIPE:
|
552
|
+
payment_transaction = payment.stripe_payment
|
553
|
+
charge_id = (
|
554
|
+
payment_transaction.payment_intent_id
|
555
|
+
if payment_transaction
|
556
|
+
else None
|
557
|
+
)
|
558
|
+
elif payment_method and payment_method.payment_provider in [
|
559
|
+
settings.TAP_CREDIT_CARD,
|
560
|
+
settings.TAP_KNET,
|
561
|
+
settings.TAP_ALL,
|
562
|
+
settings.MADA,
|
563
|
+
settings.BENEFIT,
|
564
|
+
]:
|
565
|
+
payment_transaction = payment.tap_payment
|
566
|
+
charge_id = (
|
567
|
+
payment_transaction.charge_id if payment_transaction else None
|
568
|
+
)
|
569
|
+
|
544
570
|
except ValidationError as err:
|
545
571
|
raise serializers.ValidationError(detail=err.messages)
|
546
|
-
except TapException as err:
|
547
|
-
raise serializers.ValidationError({"
|
572
|
+
except (TapException, StripeException) as err:
|
573
|
+
raise serializers.ValidationError({"payment_gateway": _(str(err))})
|
548
574
|
except ObjectDoesNotExist as err:
|
549
575
|
logger.info(
|
550
576
|
f"Payment Object not created: user:{user}, method:{payment_method}, currency:{currency}, error:{err}"
|
551
577
|
)
|
552
|
-
tap_transaction = None
|
553
578
|
|
554
579
|
return {
|
555
580
|
"orders": orders,
|
556
581
|
"payment_url": payment.payment_url,
|
557
|
-
"charge_id":
|
582
|
+
"charge_id": charge_id,
|
558
583
|
}
|
559
584
|
|
560
585
|
def create(self, validated_data: typing.Dict):
|
@@ -1654,6 +1679,7 @@ class WalletTopUpSerializer(serializers.Serializer):
|
|
1654
1679
|
store_settings.GOOGLE_PAY,
|
1655
1680
|
store_settings.MADA,
|
1656
1681
|
store_settings.BENEFIT,
|
1682
|
+
store_settings.STRIPE,
|
1657
1683
|
]
|
1658
1684
|
),
|
1659
1685
|
required=True,
|
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))
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Stripe Payment Gateway Admin
|
3
|
+
|
4
|
+
This module contains the admin configuration for Stripe payment models.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.contrib import admin
|
8
|
+
from import_export.admin import ImportExportModelAdmin
|
9
|
+
|
10
|
+
from ob_dj_store.core.stores.gateway.stripe.models import StripeCustomer, StripePayment
|
11
|
+
|
12
|
+
|
13
|
+
@admin.register(StripePayment)
|
14
|
+
class StripePaymentAdmin(ImportExportModelAdmin):
|
15
|
+
list_display = (
|
16
|
+
"payment_intent_id",
|
17
|
+
"payment",
|
18
|
+
"user",
|
19
|
+
"status",
|
20
|
+
"amount",
|
21
|
+
"currency",
|
22
|
+
"source",
|
23
|
+
"created_at",
|
24
|
+
)
|
25
|
+
list_filter = ("status", "source", "created_at")
|
26
|
+
search_fields = ("payment_intent_id", "user__email", "payment__id")
|
27
|
+
readonly_fields = (
|
28
|
+
"payment_intent_id",
|
29
|
+
"client_secret",
|
30
|
+
"init_response",
|
31
|
+
"webhook_response",
|
32
|
+
"created_at",
|
33
|
+
"updated_at",
|
34
|
+
)
|
35
|
+
raw_id_fields = ("payment", "user")
|
36
|
+
date_hierarchy = "created_at"
|
37
|
+
|
38
|
+
def get_queryset(self, request):
|
39
|
+
return (
|
40
|
+
super()
|
41
|
+
.get_queryset(request)
|
42
|
+
.select_related("payment__payment_tax", "user")
|
43
|
+
.prefetch_related(
|
44
|
+
"payment__orders__items", "payment__orders__shipping_method"
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
def amount(self, obj):
|
49
|
+
"""Display payment amount"""
|
50
|
+
return f"${obj.amount:.2f}"
|
51
|
+
|
52
|
+
amount.short_description = "Amount"
|
53
|
+
|
54
|
+
def currency(self, obj):
|
55
|
+
"""Display payment currency"""
|
56
|
+
return obj.currency.upper()
|
57
|
+
|
58
|
+
currency.short_description = "Currency"
|
59
|
+
|
60
|
+
|
61
|
+
@admin.register(StripeCustomer)
|
62
|
+
class StripeCustomerAdmin(ImportExportModelAdmin):
|
63
|
+
list_display = (
|
64
|
+
"stripe_customer_id",
|
65
|
+
"email",
|
66
|
+
"first_name",
|
67
|
+
"last_name",
|
68
|
+
"customer",
|
69
|
+
"created_at",
|
70
|
+
)
|
71
|
+
search_fields = ("stripe_customer_id", "email", "first_name", "last_name")
|
72
|
+
readonly_fields = ("stripe_customer_id", "init_data", "created_at", "updated_at")
|
73
|
+
raw_id_fields = ("customer",)
|
74
|
+
date_hierarchy = "created_at"
|
75
|
+
|
76
|
+
def get_queryset(self, request):
|
77
|
+
return super().get_queryset(request).select_related("customer")
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from django.apps import AppConfig
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
3
|
+
|
4
|
+
|
5
|
+
class StripeConfig(AppConfig):
|
6
|
+
default_auto_field = "django.db.models.BigAutoField"
|
7
|
+
name = "ob_dj_store.core.stores.gateway.stripe"
|
8
|
+
verbose_name = _("Gateway: Stripe")
|
9
|
+
label = "stripe"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""
|
2
|
+
Stripe Payment Gateway Managers
|
3
|
+
|
4
|
+
This module contains the managers for Stripe payment models.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
|
9
|
+
from django.db import models
|
10
|
+
|
11
|
+
from ob_dj_store.core.stores.gateway.stripe import utils
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class StripePaymentManager(models.Manager):
|
17
|
+
"""Manager for Stripe payments"""
|
18
|
+
|
19
|
+
def create(self, **kwargs):
|
20
|
+
"""Create Stripe payment and initiate PaymentIntent"""
|
21
|
+
user = kwargs.get("user")
|
22
|
+
payment = kwargs.get("payment")
|
23
|
+
|
24
|
+
if not user or not payment:
|
25
|
+
raise ValueError("User and payment are required")
|
26
|
+
|
27
|
+
# Initiate Stripe payment
|
28
|
+
stripe_response = utils.initiate_stripe_payment(
|
29
|
+
user=user, payment=payment, currency_code=payment.currency
|
30
|
+
)
|
31
|
+
|
32
|
+
# Merge Stripe response with kwargs
|
33
|
+
kwargs.update(stripe_response)
|
34
|
+
|
35
|
+
return super().create(**kwargs)
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# Generated by Django 3.2.8 on 2025-08-06 09:07
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
import phonenumber_field.modelfields
|
5
|
+
from django.conf import settings
|
6
|
+
from django.db import migrations, models
|
7
|
+
|
8
|
+
|
9
|
+
class Migration(migrations.Migration):
|
10
|
+
|
11
|
+
initial = True
|
12
|
+
|
13
|
+
dependencies = [
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
15
|
+
("stores", "0109_wallettransaction_cashback_type"),
|
16
|
+
]
|
17
|
+
|
18
|
+
operations = [
|
19
|
+
migrations.CreateModel(
|
20
|
+
name="StripePayment",
|
21
|
+
fields=[
|
22
|
+
(
|
23
|
+
"id",
|
24
|
+
models.BigAutoField(
|
25
|
+
auto_created=True,
|
26
|
+
primary_key=True,
|
27
|
+
serialize=False,
|
28
|
+
verbose_name="ID",
|
29
|
+
),
|
30
|
+
),
|
31
|
+
(
|
32
|
+
"payment_intent_id",
|
33
|
+
models.CharField(
|
34
|
+
db_index=True,
|
35
|
+
help_text="Stripe PaymentIntent ID",
|
36
|
+
max_length=250,
|
37
|
+
unique=True,
|
38
|
+
),
|
39
|
+
),
|
40
|
+
(
|
41
|
+
"client_secret",
|
42
|
+
models.CharField(
|
43
|
+
help_text="Client secret for frontend confirmation",
|
44
|
+
max_length=250,
|
45
|
+
),
|
46
|
+
),
|
47
|
+
(
|
48
|
+
"status",
|
49
|
+
models.CharField(
|
50
|
+
choices=[
|
51
|
+
("requires_payment_method", "Requires Payment Method"),
|
52
|
+
("requires_confirmation", "Requires Confirmation"),
|
53
|
+
("requires_action", "Requires Action"),
|
54
|
+
("processing", "Processing"),
|
55
|
+
("requires_capture", "Requires Capture"),
|
56
|
+
("canceled", "Canceled"),
|
57
|
+
("succeeded", "Succeeded"),
|
58
|
+
],
|
59
|
+
default="requires_payment_method",
|
60
|
+
max_length=50,
|
61
|
+
),
|
62
|
+
),
|
63
|
+
(
|
64
|
+
"source",
|
65
|
+
models.CharField(
|
66
|
+
choices=[
|
67
|
+
("card", "Credit/Debit Card"),
|
68
|
+
("apple_pay", "Apple Pay"),
|
69
|
+
("google_pay", "Google Pay"),
|
70
|
+
("ach_debit", "ACH Bank Transfer"),
|
71
|
+
("klarna", "Klarna"),
|
72
|
+
("afterpay_clearpay", "Afterpay"),
|
73
|
+
],
|
74
|
+
default="card",
|
75
|
+
max_length=50,
|
76
|
+
),
|
77
|
+
),
|
78
|
+
(
|
79
|
+
"init_response",
|
80
|
+
models.JSONField(
|
81
|
+
blank=True,
|
82
|
+
help_text="Initial PaymentIntent response from Stripe",
|
83
|
+
null=True,
|
84
|
+
),
|
85
|
+
),
|
86
|
+
(
|
87
|
+
"webhook_response",
|
88
|
+
models.JSONField(
|
89
|
+
blank=True,
|
90
|
+
help_text="Final webhook response from Stripe",
|
91
|
+
null=True,
|
92
|
+
),
|
93
|
+
),
|
94
|
+
(
|
95
|
+
"langid",
|
96
|
+
models.CharField(
|
97
|
+
default="EN", help_text="Language preference", max_length=10
|
98
|
+
),
|
99
|
+
),
|
100
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
101
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
102
|
+
(
|
103
|
+
"payment",
|
104
|
+
models.OneToOneField(
|
105
|
+
help_text="Reference to local payment transaction",
|
106
|
+
on_delete=django.db.models.deletion.CASCADE,
|
107
|
+
related_name="stripe_payment",
|
108
|
+
to="stores.payment",
|
109
|
+
),
|
110
|
+
),
|
111
|
+
(
|
112
|
+
"user",
|
113
|
+
models.ForeignKey(
|
114
|
+
help_text="User who initiated the payment",
|
115
|
+
on_delete=django.db.models.deletion.CASCADE,
|
116
|
+
to=settings.AUTH_USER_MODEL,
|
117
|
+
),
|
118
|
+
),
|
119
|
+
],
|
120
|
+
options={
|
121
|
+
"verbose_name": "Stripe Payment",
|
122
|
+
"verbose_name_plural": "Stripe Payments",
|
123
|
+
"ordering": ["-created_at"],
|
124
|
+
},
|
125
|
+
),
|
126
|
+
migrations.CreateModel(
|
127
|
+
name="StripeCustomer",
|
128
|
+
fields=[
|
129
|
+
(
|
130
|
+
"id",
|
131
|
+
models.BigAutoField(
|
132
|
+
auto_created=True,
|
133
|
+
primary_key=True,
|
134
|
+
serialize=False,
|
135
|
+
verbose_name="ID",
|
136
|
+
),
|
137
|
+
),
|
138
|
+
(
|
139
|
+
"stripe_customer_id",
|
140
|
+
models.CharField(db_index=True, max_length=200, unique=True),
|
141
|
+
),
|
142
|
+
("first_name", models.CharField(max_length=200)),
|
143
|
+
("last_name", models.CharField(max_length=200)),
|
144
|
+
("email", models.EmailField(max_length=254, null=True, unique=True)),
|
145
|
+
(
|
146
|
+
"phone_number",
|
147
|
+
phonenumber_field.modelfields.PhoneNumberField(
|
148
|
+
max_length=128, null=True, region=None, unique=True
|
149
|
+
),
|
150
|
+
),
|
151
|
+
("init_data", models.JSONField()),
|
152
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
153
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
154
|
+
(
|
155
|
+
"customer",
|
156
|
+
models.OneToOneField(
|
157
|
+
on_delete=django.db.models.deletion.CASCADE,
|
158
|
+
related_name="stripe_customer",
|
159
|
+
to=settings.AUTH_USER_MODEL,
|
160
|
+
),
|
161
|
+
),
|
162
|
+
],
|
163
|
+
options={
|
164
|
+
"verbose_name": "Stripe Customer",
|
165
|
+
"verbose_name_plural": "Stripe Customers",
|
166
|
+
},
|
167
|
+
),
|
168
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
# Migrations for Stripe payment gateway
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""
|
2
|
+
Stripe Payment Gateway Models
|
3
|
+
|
4
|
+
This module contains the models for Stripe payment integration,
|
5
|
+
mirroring the structure of the TAP payment gateway.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
|
10
|
+
from django.conf import settings
|
11
|
+
from django.db import models
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
13
|
+
from phonenumber_field.modelfields import PhoneNumberField
|
14
|
+
|
15
|
+
from ob_dj_store.core.stores.gateway.stripe.managers import StripePaymentManager
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class StripePayment(models.Model):
|
21
|
+
"""StripePayment captures the payment from Stripe"""
|
22
|
+
|
23
|
+
class Status(models.TextChoices):
|
24
|
+
REQUIRES_PAYMENT_METHOD = (
|
25
|
+
"requires_payment_method",
|
26
|
+
_("Requires Payment Method"),
|
27
|
+
)
|
28
|
+
REQUIRES_CONFIRMATION = "requires_confirmation", _("Requires Confirmation")
|
29
|
+
REQUIRES_ACTION = "requires_action", _("Requires Action")
|
30
|
+
PROCESSING = "processing", _("Processing")
|
31
|
+
REQUIRES_CAPTURE = "requires_capture", _("Requires Capture")
|
32
|
+
CANCELED = "canceled", _("Canceled")
|
33
|
+
SUCCEEDED = "succeeded", _("Succeeded")
|
34
|
+
|
35
|
+
class Sources(models.TextChoices):
|
36
|
+
CARD = "card", _("Credit/Debit Card")
|
37
|
+
APPLE_PAY = "apple_pay", _("Apple Pay")
|
38
|
+
GOOGLE_PAY = "google_pay", _("Google Pay")
|
39
|
+
ACH_DEBIT = "ach_debit", _("ACH Bank Transfer")
|
40
|
+
KLARNA = "klarna", _("Klarna")
|
41
|
+
AFTERPAY = "afterpay_clearpay", _("Afterpay")
|
42
|
+
|
43
|
+
# Core fields
|
44
|
+
payment = models.OneToOneField(
|
45
|
+
"stores.Payment",
|
46
|
+
on_delete=models.CASCADE,
|
47
|
+
related_name="stripe_payment",
|
48
|
+
help_text=_("Reference to local payment transaction"),
|
49
|
+
)
|
50
|
+
user = models.ForeignKey(
|
51
|
+
settings.AUTH_USER_MODEL,
|
52
|
+
on_delete=models.CASCADE,
|
53
|
+
help_text=_("User who initiated the payment"),
|
54
|
+
)
|
55
|
+
|
56
|
+
# Stripe specific fields
|
57
|
+
payment_intent_id = models.CharField(
|
58
|
+
max_length=250,
|
59
|
+
unique=True,
|
60
|
+
db_index=True,
|
61
|
+
help_text=_("Stripe PaymentIntent ID"),
|
62
|
+
)
|
63
|
+
client_secret = models.CharField(
|
64
|
+
max_length=250, help_text=_("Client secret for frontend confirmation")
|
65
|
+
)
|
66
|
+
status = models.CharField(
|
67
|
+
max_length=50, choices=Status.choices, default=Status.REQUIRES_PAYMENT_METHOD
|
68
|
+
)
|
69
|
+
source = models.CharField(
|
70
|
+
max_length=50, choices=Sources.choices, default=Sources.CARD
|
71
|
+
)
|
72
|
+
|
73
|
+
# Response data
|
74
|
+
init_response = models.JSONField(
|
75
|
+
help_text=_("Initial PaymentIntent response from Stripe"), null=True, blank=True
|
76
|
+
)
|
77
|
+
webhook_response = models.JSONField(
|
78
|
+
help_text=_("Final webhook response from Stripe"), null=True, blank=True
|
79
|
+
)
|
80
|
+
|
81
|
+
# Metadata
|
82
|
+
langid = models.CharField(
|
83
|
+
max_length=10, default="EN", help_text=_("Language preference")
|
84
|
+
)
|
85
|
+
|
86
|
+
# Audit fields
|
87
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
88
|
+
updated_at = models.DateTimeField(auto_now=True)
|
89
|
+
|
90
|
+
objects = StripePaymentManager()
|
91
|
+
|
92
|
+
class Meta:
|
93
|
+
verbose_name = _("Stripe Payment")
|
94
|
+
verbose_name_plural = _("Stripe Payments")
|
95
|
+
ordering = ["-created_at"]
|
96
|
+
|
97
|
+
def __str__(self):
|
98
|
+
return f"StripePayment {self.payment_intent_id}"
|
99
|
+
|
100
|
+
@property
|
101
|
+
def amount(self):
|
102
|
+
"""Return payment amount in dollars"""
|
103
|
+
return self.payment.total_payment
|
104
|
+
|
105
|
+
@property
|
106
|
+
def amount_cents(self):
|
107
|
+
"""Return payment amount in cents for Stripe"""
|
108
|
+
return int(self.amount * 100)
|
109
|
+
|
110
|
+
@property
|
111
|
+
def currency(self):
|
112
|
+
"""Get currency from payment"""
|
113
|
+
return self.payment.currency.lower()
|
114
|
+
|
115
|
+
@property
|
116
|
+
def payment_url(self):
|
117
|
+
"""Return client secret for frontend processing"""
|
118
|
+
return self.client_secret
|
119
|
+
|
120
|
+
def webhook_update(self, stripe_event):
|
121
|
+
"""Update payment based on Stripe webhook"""
|
122
|
+
payment_intent = stripe_event["data"]["object"]
|
123
|
+
|
124
|
+
# Update status and webhook response
|
125
|
+
old_status = self.status
|
126
|
+
self.status = payment_intent["status"]
|
127
|
+
self.webhook_response = payment_intent
|
128
|
+
self.save()
|
129
|
+
|
130
|
+
# Mark transaction based on new status
|
131
|
+
self.mark_transaction()
|
132
|
+
|
133
|
+
logger.info(
|
134
|
+
f"Stripe webhook updated PaymentIntent {self.payment_intent_id}: {old_status} -> {self.status}"
|
135
|
+
)
|
136
|
+
|
137
|
+
def mark_transaction(self):
|
138
|
+
"""Mark the associated payment based on Stripe status"""
|
139
|
+
if self.status == self.Status.SUCCEEDED:
|
140
|
+
self.payment.mark_paid()
|
141
|
+
elif self.status in [self.Status.CANCELED]:
|
142
|
+
error_message = self.webhook_response.get("last_payment_error", {}).get(
|
143
|
+
"message", "Payment failed"
|
144
|
+
)
|
145
|
+
self.payment.mark_failed(error_message)
|
146
|
+
|
147
|
+
|
148
|
+
class StripeCustomer(models.Model):
|
149
|
+
"""Stripe customer mapping"""
|
150
|
+
|
151
|
+
customer = models.OneToOneField(
|
152
|
+
settings.AUTH_USER_MODEL,
|
153
|
+
on_delete=models.CASCADE,
|
154
|
+
related_name="stripe_customer",
|
155
|
+
)
|
156
|
+
stripe_customer_id = models.CharField(max_length=200, unique=True, db_index=True)
|
157
|
+
first_name = models.CharField(max_length=200)
|
158
|
+
last_name = models.CharField(max_length=200)
|
159
|
+
email = models.EmailField(unique=True, null=True)
|
160
|
+
phone_number = PhoneNumberField(unique=True, null=True)
|
161
|
+
|
162
|
+
# Store original Stripe response
|
163
|
+
init_data = models.JSONField()
|
164
|
+
|
165
|
+
# Audit fields
|
166
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
167
|
+
updated_at = models.DateTimeField(auto_now=True)
|
168
|
+
|
169
|
+
class Meta:
|
170
|
+
verbose_name = _("Stripe Customer")
|
171
|
+
verbose_name_plural = _("Stripe Customers")
|
172
|
+
|
173
|
+
def __str__(self):
|
174
|
+
return f"{self.email} | {self.stripe_customer_id}"
|
@@ -0,0 +1,170 @@
|
|
1
|
+
"""
|
2
|
+
Stripe Payment Gateway Utilities
|
3
|
+
|
4
|
+
This module contains utility functions for Stripe payment integration.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
|
9
|
+
import stripe
|
10
|
+
from django.conf import settings
|
11
|
+
from django.contrib.auth import get_user_model
|
12
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
13
|
+
|
14
|
+
User = get_user_model()
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
# Configure Stripe
|
18
|
+
stripe.api_key = settings.STRIPE_SECRET_KEY
|
19
|
+
stripe.api_version = settings.STRIPE_API_VERSION
|
20
|
+
|
21
|
+
|
22
|
+
class StripeException(Exception):
|
23
|
+
"""Custom exception for Stripe-related errors"""
|
24
|
+
|
25
|
+
|
26
|
+
def get_or_create_stripe_customer(user):
|
27
|
+
"""Get or create a Stripe customer for the user"""
|
28
|
+
from ob_dj_store.core.stores.gateway.stripe.models import StripeCustomer
|
29
|
+
|
30
|
+
try:
|
31
|
+
return StripeCustomer.objects.get(customer=user)
|
32
|
+
except ObjectDoesNotExist:
|
33
|
+
pass
|
34
|
+
|
35
|
+
# Create customer in Stripe
|
36
|
+
try:
|
37
|
+
customer_data = {
|
38
|
+
"name": f"{user.first_name} {user.last_name}".strip(),
|
39
|
+
"email": user.email,
|
40
|
+
"metadata": {"user_id": str(user.id), "platform": "ob-dj-store"},
|
41
|
+
}
|
42
|
+
|
43
|
+
# Add phone if available
|
44
|
+
phone_number = getattr(user, "phone_number", None)
|
45
|
+
if phone_number:
|
46
|
+
customer_data["phone"] = str(phone_number)
|
47
|
+
|
48
|
+
stripe_customer = stripe.Customer.create(**customer_data)
|
49
|
+
|
50
|
+
# Save to database
|
51
|
+
customer_record = StripeCustomer.objects.create(
|
52
|
+
customer=user,
|
53
|
+
stripe_customer_id=stripe_customer.id,
|
54
|
+
first_name=user.first_name,
|
55
|
+
last_name=user.last_name,
|
56
|
+
email=user.email,
|
57
|
+
phone_number=phone_number,
|
58
|
+
init_data=stripe_customer,
|
59
|
+
)
|
60
|
+
|
61
|
+
logger.info(f"Created Stripe customer {stripe_customer.id} for user {user.id}")
|
62
|
+
return customer_record
|
63
|
+
|
64
|
+
except stripe.error.StripeError as e:
|
65
|
+
logger.error(f"Failed to create Stripe customer: {str(e)}")
|
66
|
+
raise StripeException(f"Failed to create customer: {str(e)}")
|
67
|
+
|
68
|
+
|
69
|
+
def initiate_stripe_payment(user, payment, currency_code):
|
70
|
+
"""Initiate a Stripe PaymentIntent"""
|
71
|
+
|
72
|
+
try:
|
73
|
+
# Get or create Stripe customer
|
74
|
+
stripe_customer = get_or_create_stripe_customer(user)
|
75
|
+
|
76
|
+
# Get the first order for metadata
|
77
|
+
order = payment.orders.first()
|
78
|
+
|
79
|
+
# Prepare PaymentIntent data
|
80
|
+
intent_data = {
|
81
|
+
"amount": int(payment.total_payment * 100), # Convert to cents
|
82
|
+
"currency": currency_code.lower(),
|
83
|
+
"customer": stripe_customer.stripe_customer_id,
|
84
|
+
"metadata": {
|
85
|
+
"payment_id": str(payment.id),
|
86
|
+
"user_id": str(user.id),
|
87
|
+
"order_id": str(order.id) if order else None,
|
88
|
+
"platform": "ob-dj-store",
|
89
|
+
},
|
90
|
+
"automatic_payment_methods": {"enabled": True,},
|
91
|
+
}
|
92
|
+
|
93
|
+
# Add description
|
94
|
+
if order:
|
95
|
+
intent_data[
|
96
|
+
"description"
|
97
|
+
] = f"Order #{order.id} from {order.store.name if order.store else 'Store'}"
|
98
|
+
|
99
|
+
# Create PaymentIntent
|
100
|
+
payment_intent = stripe.PaymentIntent.create(**intent_data)
|
101
|
+
|
102
|
+
logger.info(
|
103
|
+
f"Created Stripe PaymentIntent {payment_intent.id} for payment {payment.id}"
|
104
|
+
)
|
105
|
+
|
106
|
+
return {
|
107
|
+
"payment_intent_id": payment_intent.id,
|
108
|
+
"client_secret": payment_intent.client_secret,
|
109
|
+
"status": payment_intent.status,
|
110
|
+
"init_response": payment_intent,
|
111
|
+
}
|
112
|
+
|
113
|
+
except stripe.error.StripeError as e:
|
114
|
+
logger.error(f"Failed to create PaymentIntent: {str(e)}")
|
115
|
+
raise StripeException(f"Failed to initiate payment: {str(e)}")
|
116
|
+
|
117
|
+
|
118
|
+
def handle_stripe_webhook(event_data):
|
119
|
+
"""Handle Stripe webhook events"""
|
120
|
+
from ob_dj_store.core.stores.gateway.stripe.models import StripePayment
|
121
|
+
|
122
|
+
event_type = event_data.get("type")
|
123
|
+
|
124
|
+
# List of payment_intent events we handle
|
125
|
+
payment_intent_events = [
|
126
|
+
"payment_intent.succeeded",
|
127
|
+
"payment_intent.payment_failed",
|
128
|
+
"payment_intent.canceled",
|
129
|
+
"payment_intent.requires_action",
|
130
|
+
"payment_intent.processing",
|
131
|
+
]
|
132
|
+
|
133
|
+
if event_type in payment_intent_events:
|
134
|
+
payment_intent = event_data["data"]["object"]
|
135
|
+
payment_intent_id = payment_intent["id"]
|
136
|
+
|
137
|
+
try:
|
138
|
+
stripe_payment = StripePayment.objects.get(
|
139
|
+
payment_intent_id=payment_intent_id
|
140
|
+
)
|
141
|
+
stripe_payment.webhook_update(event_data)
|
142
|
+
logger.info(
|
143
|
+
f"Successfully processed {event_type} for PaymentIntent {payment_intent_id}"
|
144
|
+
)
|
145
|
+
return True
|
146
|
+
except ObjectDoesNotExist:
|
147
|
+
logger.warning(
|
148
|
+
f"Received webhook for unknown PaymentIntent: {payment_intent_id}"
|
149
|
+
)
|
150
|
+
# Return True for unknown payments to acknowledge webhook (don't retry)
|
151
|
+
return True
|
152
|
+
|
153
|
+
# Log unhandled events but return True (acknowledged)
|
154
|
+
logger.info(f"Unhandled Stripe webhook event: {event_type}")
|
155
|
+
return True
|
156
|
+
|
157
|
+
|
158
|
+
def verify_webhook_signature(payload, signature):
|
159
|
+
"""Verify Stripe webhook signature and return event data"""
|
160
|
+
try:
|
161
|
+
event = stripe.Webhook.construct_event(
|
162
|
+
payload, signature, settings.STRIPE_WEBHOOK_SECRET
|
163
|
+
)
|
164
|
+
return event
|
165
|
+
except ValueError:
|
166
|
+
logger.error("Invalid webhook payload")
|
167
|
+
raise ValidationError("Invalid payload")
|
168
|
+
except stripe.error.SignatureVerificationError:
|
169
|
+
logger.error("Invalid webhook signature")
|
170
|
+
raise ValidationError("Invalid signature")
|
@@ -180,4 +180,8 @@ class Payment(models.Model):
|
|
180
180
|
settings.BENEFIT,
|
181
181
|
]:
|
182
182
|
payment_url = self.tap_payment.payment_url
|
183
|
+
elif gateway == settings.STRIPE:
|
184
|
+
# For Stripe, return the client_secret which is used by Stripe.js
|
185
|
+
if hasattr(self, "stripe_payment"):
|
186
|
+
payment_url = self.stripe_payment.payment_url
|
183
187
|
return payment_url
|
@@ -250,10 +250,16 @@ class Product(DjangoModelCleanMixin, models.Model):
|
|
250
250
|
|
251
251
|
def get_inventory(self, store_id):
|
252
252
|
try:
|
253
|
-
|
253
|
+
# Prefer the primary variant if available
|
254
|
+
product_variant = (
|
255
|
+
self.product_variants.filter(inventories__is_primary=True).first()
|
256
|
+
or self.product_variants.first()
|
257
|
+
)
|
258
|
+
if not product_variant:
|
259
|
+
return None
|
260
|
+
return product_variant.inventories.get(store_id=store_id)
|
254
261
|
except ObjectDoesNotExist:
|
255
262
|
return None
|
256
|
-
return inventory
|
257
263
|
|
258
264
|
def is_snoozed(self, store_id):
|
259
265
|
try:
|
@@ -3,7 +3,11 @@ ob_dj_store/apis/stores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
3
3
|
ob_dj_store/apis/stores/filters.py,sha256=JeUtcicJ3cFnZc71aUMEwn2jaVj1oNQsZOCZfdxGDu8,10338
|
4
4
|
ob_dj_store/apis/stores/urls.py,sha256=W9sNJ7ST9pGnfVCf7GmduDO-_zK9MGTI6ybV8n-lzSc,2111
|
5
5
|
ob_dj_store/apis/stores/views.py,sha256=cwDFNY2rtTfjl_F9PpR5zOJy2DPmGidOjIlJ9fsgSYI,42744
|
6
|
-
ob_dj_store/apis/stores/rest/serializers/serializers.py,sha256=
|
6
|
+
ob_dj_store/apis/stores/rest/serializers/serializers.py,sha256=rJ3oWg0GqbZ8OoPWRTxcuPkIRYPQPfK4TH4UH4vsjns,69192
|
7
|
+
ob_dj_store/apis/stripe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
ob_dj_store/apis/stripe/serializers.py,sha256=SX3yKVNbDvE5LL35DF9XoC8sjwGkCSdKz_JUVWsJDLg,5949
|
9
|
+
ob_dj_store/apis/stripe/urls.py,sha256=IHH9sX3d74KDUlXkYnIAwYRC6zEvUaOTSFsJGJfiAjA,723
|
10
|
+
ob_dj_store/apis/stripe/views.py,sha256=RnGfp77M2AwoEhmkIUPNY7xoiH0MODMUNF7e9uvt12g,6913
|
7
11
|
ob_dj_store/apis/tap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
12
|
ob_dj_store/apis/tap/serializers.py,sha256=KPrBK4h2-fWvEVf6vOj2ww5-USV9WqpyYicIqoHIiXI,1065
|
9
13
|
ob_dj_store/apis/tap/urls.py,sha256=bnOTv6an11kxpo_FdqlhsizlGPLVpNxBjCyKcf3_C9M,367
|
@@ -18,6 +22,14 @@ ob_dj_store/core/stores/receivers.py,sha256=vduxOB9M6IgQ8E7dYrbC4T0PjOqLCgAVi2Pw
|
|
18
22
|
ob_dj_store/core/stores/settings_validation.py,sha256=eTkRaI6CG5OEJQyI5CF-cNAcvjzXf3GwX5sR97O3v98,3977
|
19
23
|
ob_dj_store/core/stores/utils.py,sha256=r6YdjQu5gsrZmO_qgXb21xcB50tbAI9vffN4Yp6n6iA,3427
|
20
24
|
ob_dj_store/core/stores/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
+
ob_dj_store/core/stores/gateway/stripe/__init__.py,sha256=Plk4Ur6tDpc7Cz2YdgDtu6Y6yHUZR2Vpxsq9bysoZ8g,105
|
26
|
+
ob_dj_store/core/stores/gateway/stripe/admin.py,sha256=gQ4orM5Hm0LquKP38XeCsm4vLRSGQr6KO-hEe_uNVHY,2119
|
27
|
+
ob_dj_store/core/stores/gateway/stripe/apps.py,sha256=UYt5Kj_bi1Y-lAPVi_8eJbyiINESy_Os-UqOX2UT4GI,292
|
28
|
+
ob_dj_store/core/stores/gateway/stripe/managers.py,sha256=tEU3NlKWz9Vv8MJrkugeHUo8Zk-ThG0q993Rjr_0kCw,895
|
29
|
+
ob_dj_store/core/stores/gateway/stripe/models.py,sha256=-A_Inq6NsFengz4zv7mBDt6P9BP_XS7jN6pBnZ97GqM,5597
|
30
|
+
ob_dj_store/core/stores/gateway/stripe/utils.py,sha256=u3nU2UQbgxkbrjIe6OiuB6zseiFhXXX0lP4Cx0ofpWU,5581
|
31
|
+
ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py,sha256=dlzfmrt3zyFJHMUxSt8QADl-TkpHVRgvfbl8GPyy6Ws,6274
|
32
|
+
ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py,sha256=-hjs2JUX2zw379yapyaYFiunRtwtqeYnBkiiQS9HA3o,40
|
21
33
|
ob_dj_store/core/stores/gateway/tap/__init__.py,sha256=5Z6azpb6tmr1nRvKwQWzlYw9ruvw-9ZMBWRqEngDKTM,40
|
22
34
|
ob_dj_store/core/stores/gateway/tap/admin.py,sha256=tGtLg6ttkZXb7JtFPD-5r46pCmb1BsbQpv182KlY3-o,1153
|
23
35
|
ob_dj_store/core/stores/gateway/tap/apps.py,sha256=kWwPjuAJeEmEasVDzUbvRsGaQWL-aYe4JDHNLvCVXPs,212
|
@@ -152,15 +164,15 @@ ob_dj_store/core/stores/models/_feedback.py,sha256=5kQ_AD9nkY57UOKo_qHg4_nEQAdFj
|
|
152
164
|
ob_dj_store/core/stores/models/_inventory.py,sha256=_rGlVL5HjOlHQVB8CI0776CPcE5r6szv1w3PjtkYnWM,4306
|
153
165
|
ob_dj_store/core/stores/models/_order.py,sha256=_08lqX5p4brCdUilqfbT--6Ao2TMsqnzXaWoV_7IYL8,9758
|
154
166
|
ob_dj_store/core/stores/models/_partner.py,sha256=OuYvevUWn1sYHs9PcFf51EUUC1uqytQss8Bx91aMOH8,4732
|
155
|
-
ob_dj_store/core/stores/models/_payment.py,sha256=
|
156
|
-
ob_dj_store/core/stores/models/_product.py,sha256=
|
167
|
+
ob_dj_store/core/stores/models/_payment.py,sha256=SfAMVrdP2Hfz604AyO8EOjeiOgGMeJ8My0s5U4GZ7Yc,6650
|
168
|
+
ob_dj_store/core/stores/models/_product.py,sha256=KUi2hkA4VNCrWwWp3AM_tkubFptpOdiFt0Twzi2wW14,18535
|
157
169
|
ob_dj_store/core/stores/models/_store.py,sha256=0K-CNJWuXNqeyULL1J0M9hiNcVla0UNNjdCdN_nzNEE,9833
|
158
170
|
ob_dj_store/core/stores/models/_wallet.py,sha256=YvT-rvED-jrYjePLJpvdLXXoBudR6TGPu5cNE0m2fWo,5643
|
159
171
|
ob_dj_store/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
160
172
|
ob_dj_store/utils/helpers.py,sha256=o7wgypM7mI2vZqZKkhxnTcnHJC8GMQDOuYMnRwXr6tY,2058
|
161
173
|
ob_dj_store/utils/model.py,sha256=DV7hOhTaZL3gh9sptts2jTUFlTArKG3i7oPioq9HLFE,303
|
162
174
|
ob_dj_store/utils/utils.py,sha256=8UVAFB56qUSjJJ5f9vnermtw638gdFy4CFRCuMbns_M,1342
|
163
|
-
ob_dj_store-0.0.21.
|
164
|
-
ob_dj_store-0.0.21.
|
165
|
-
ob_dj_store-0.0.21.
|
166
|
-
ob_dj_store-0.0.21.
|
175
|
+
ob_dj_store-0.0.21.6.dist-info/METADATA,sha256=suIPKWs6v3NePNuNpqbqF57ezgAuw3p34iphhABaMfI,2850
|
176
|
+
ob_dj_store-0.0.21.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
177
|
+
ob_dj_store-0.0.21.6.dist-info/top_level.txt,sha256=CZG3G0ptTkzGnc0dFYN-ZD7YKdJBmm47bsmGwofD_lk,12
|
178
|
+
ob_dj_store-0.0.21.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|