ob-dj-store 0.0.19__py3-none-any.whl → 0.0.23.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ob_dj_store/apis/stores/filters.py +42 -19
- ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
- ob_dj_store/apis/stores/urls.py +6 -0
- ob_dj_store/apis/stores/views.py +140 -227
- ob_dj_store/apis/stripe/__init__.py +0 -0
- ob_dj_store/apis/stripe/serializers.py +185 -0
- ob_dj_store/apis/stripe/urls.py +25 -0
- ob_dj_store/apis/stripe/views.py +191 -0
- ob_dj_store/apis/tap/views.py +2 -6
- ob_dj_store/core/stores/admin.py +41 -38
- ob_dj_store/core/stores/admin_inlines.py +8 -13
- ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
- ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
- ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
- ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
- ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
- ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
- ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
- ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
- ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
- ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
- ob_dj_store/core/stores/gateway/tap/models.py +4 -13
- ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
- ob_dj_store/core/stores/managers.py +12 -4
- ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
- ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
- ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
- ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
- ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
- ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
- ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
- ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
- ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
- ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
- ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
- ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
- ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
- ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
- ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
- ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
- ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
- ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
- ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
- ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
- ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
- ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
- ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
- ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
- ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
- ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
- ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
- ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
- ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
- ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
- ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
- ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
- ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
- ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
- ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
- ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
- ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
- ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
- ob_dj_store/core/stores/models/__init__.py +9 -1
- ob_dj_store/core/stores/models/_address.py +1 -3
- ob_dj_store/core/stores/models/_cart.py +11 -5
- ob_dj_store/core/stores/models/_feedback.py +1 -3
- ob_dj_store/core/stores/models/_inventory.py +3 -2
- ob_dj_store/core/stores/models/_order.py +69 -20
- ob_dj_store/core/stores/models/_payment.py +28 -24
- ob_dj_store/core/stores/models/_product.py +31 -17
- ob_dj_store/core/stores/models/_store.py +9 -13
- ob_dj_store/core/stores/models/_wallet.py +34 -26
- ob_dj_store/core/stores/receivers.py +43 -27
- ob_dj_store/core/stores/utils.py +1 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
|
@@ -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)
|