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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. ob_dj_store/apis/stores/filters.py +42 -19
  2. ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
  3. ob_dj_store/apis/stores/urls.py +6 -0
  4. ob_dj_store/apis/stores/views.py +140 -227
  5. ob_dj_store/apis/stripe/__init__.py +0 -0
  6. ob_dj_store/apis/stripe/serializers.py +185 -0
  7. ob_dj_store/apis/stripe/urls.py +25 -0
  8. ob_dj_store/apis/stripe/views.py +191 -0
  9. ob_dj_store/apis/tap/views.py +2 -6
  10. ob_dj_store/core/stores/admin.py +41 -38
  11. ob_dj_store/core/stores/admin_inlines.py +8 -13
  12. ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
  13. ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
  14. ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
  15. ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
  16. ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
  17. ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
  18. ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
  19. ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
  20. ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
  21. ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
  22. ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
  23. ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
  24. ob_dj_store/core/stores/gateway/tap/models.py +4 -13
  25. ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
  26. ob_dj_store/core/stores/managers.py +12 -4
  27. ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
  28. ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
  29. ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
  30. ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
  31. ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
  32. ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
  33. ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
  34. ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
  35. ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
  36. ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
  37. ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
  38. ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
  39. ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
  40. ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
  41. ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
  42. ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
  43. ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
  44. ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
  45. ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
  46. ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
  47. ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
  48. ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
  49. ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
  50. ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
  51. ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
  52. ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
  53. ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
  54. ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
  55. ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
  56. ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
  57. ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
  58. ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
  59. ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
  60. ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
  61. ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
  62. ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
  63. ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
  64. ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
  65. ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
  66. ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
  67. ob_dj_store/core/stores/models/__init__.py +9 -1
  68. ob_dj_store/core/stores/models/_address.py +1 -3
  69. ob_dj_store/core/stores/models/_cart.py +11 -5
  70. ob_dj_store/core/stores/models/_feedback.py +1 -3
  71. ob_dj_store/core/stores/models/_inventory.py +3 -2
  72. ob_dj_store/core/stores/models/_order.py +69 -20
  73. ob_dj_store/core/stores/models/_payment.py +28 -24
  74. ob_dj_store/core/stores/models/_product.py +31 -17
  75. ob_dj_store/core/stores/models/_store.py +9 -13
  76. ob_dj_store/core/stores/models/_wallet.py +34 -26
  77. ob_dj_store/core/stores/receivers.py +43 -27
  78. ob_dj_store/core/stores/utils.py +1 -2
  79. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
  80. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
  81. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
  82. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
@@ -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")
@@ -25,9 +25,7 @@ class TapPaymentAdmin(ImportExportModelAdmin, admin.ModelAdmin):
25
25
  return (
26
26
  super()
27
27
  .get_queryset(request)
28
- .select_related(
29
- "payment__payment_tax",
30
- )
28
+ .select_related("payment__payment_tax",)
31
29
  .prefetch_related(
32
30
  "payment__orders__items", "payment__orders__shipping_method"
33
31
  )
@@ -18,11 +18,6 @@ class TapPaymentManager(models.Manager):
18
18
  source = kwargs.pop("source")
19
19
  payment = kwargs.get("payment")
20
20
  user = kwargs.get("user")
21
- tap_response = utils.initiate_payment(
22
- source,
23
- user,
24
- payment,
25
- payment.currency,
26
- )
21
+ tap_response = utils.initiate_payment(source, user, payment, payment.currency,)
27
22
  kwargs = {**tap_response, **kwargs}
28
23
  return super(TapPaymentManager, self).create(**kwargs)
@@ -127,8 +127,6 @@ class Migration(migrations.Migration):
127
127
  ),
128
128
  ),
129
129
  ],
130
- options={
131
- "verbose_name": "TAP Payment",
132
- },
130
+ options={"verbose_name": "TAP Payment",},
133
131
  ),
134
132
  ]