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.
@@ -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
- tap_transaction = payment.tap_payment
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({"tap": _(str(err))})
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": tap_transaction.charge_id if tap_transaction else None,
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,2 @@
1
+ # Stripe Payment Gateway
2
+ default_app_config = "ob_dj_store.core.stores.gateway.stripe.apps.StripeConfig"
@@ -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
- inventory = self.product_variants.first().inventories.get(store_id=store_id)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ob-dj-store
3
- Version: 0.0.21.4
3
+ Version: 0.0.21.6
4
4
  Summary: OBytes django application for managing ecommerce stores.
5
5
  Home-page: https://www.obytes.com/
6
6
  Author: OBytes
@@ -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=R_FGdD98iLWsb0dsI-kdq9_60yPkwahT_kMpfNPUzmQ,68239
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=FTV-NmvQjxwwR5C5X7qYWV-ZUIZfqMMEjkBNaS-drLs,6421
156
- ob_dj_store/core/stores/models/_product.py,sha256=cYMsCtcYoJJ-kdmDmHGm2AGBo2cn6qWuF9g7zQJ_p2g,18283
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.4.dist-info/METADATA,sha256=qhdG-tlBDz4MV9ybB8VQwDWe5t-2eKMTacinZNLizjo,2850
164
- ob_dj_store-0.0.21.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
165
- ob_dj_store-0.0.21.4.dist-info/top_level.txt,sha256=CZG3G0ptTkzGnc0dFYN-ZD7YKdJBmm47bsmGwofD_lk,12
166
- ob_dj_store-0.0.21.4.dist-info/RECORD,,
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,,