django-stripe-hooks 0.0.1.dev1__tar.gz
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.
- django_stripe_hooks-0.0.1.dev1/PKG-INFO +51 -0
- django_stripe_hooks-0.0.1.dev1/README.md +33 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/__init__.py +0 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/admin.py +484 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/apps.py +24 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/migrations/0001_initial.py +361 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/migrations/__init__.py +0 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/models.py +1516 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/urls.py +12 -0
- django_stripe_hooks-0.0.1.dev1/django_stripe_hooks/views.py +115 -0
- django_stripe_hooks-0.0.1.dev1/pyproject.toml +55 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-stripe-hooks
|
|
3
|
+
Version: 0.0.1.dev1
|
|
4
|
+
Summary: Webhooks for syncing Stripe payment data with your Django database.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Geoffrey Eisenbarth
|
|
7
|
+
Author-email: geoffrey.eisenbarth@gmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: django (>=6.0)
|
|
15
|
+
Requires-Dist: stripe (>=14.4.1,<15.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Django Stripe Hooks
|
|
19
|
+
|
|
20
|
+
TODO
|
|
21
|
+
|
|
22
|
+
## Installing
|
|
23
|
+
|
|
24
|
+
1) Add to `INSTALLED_APPS`:
|
|
25
|
+
|
|
26
|
+
2) Add to `urls.py`:
|
|
27
|
+
|
|
28
|
+
`path('stripe/', include('django_stripe_hooks.urls')),`
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Contributing
|
|
32
|
+
|
|
33
|
+
### Running Tests
|
|
34
|
+
|
|
35
|
+
1) The test environment requires Stripe CLI package to be installed.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
curl -L https://github.com/stripe/stripe-cli/releases/download/v1.39.0/stripe_1.39.0_linux_x86_64.tar.gz -o stripe.tar.gz
|
|
39
|
+
sudo tar -xvf stripe.tar.gz -C /usr/local/bin
|
|
40
|
+
rm stripe.tar.gz
|
|
41
|
+
|
|
42
|
+
stripe --version
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2) Run test: `poetry run pytest -s`
|
|
47
|
+
|
|
48
|
+
To generate coverage report, use `poetry run pytest --cov --cov-branch --cov-report=xml`
|
|
49
|
+
|
|
50
|
+
Whenever tests fail, you can check `stripe_cli.log` for more details.
|
|
51
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Django Stripe Hooks
|
|
2
|
+
|
|
3
|
+
TODO
|
|
4
|
+
|
|
5
|
+
## Installing
|
|
6
|
+
|
|
7
|
+
1) Add to `INSTALLED_APPS`:
|
|
8
|
+
|
|
9
|
+
2) Add to `urls.py`:
|
|
10
|
+
|
|
11
|
+
`path('stripe/', include('django_stripe_hooks.urls')),`
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Contributing
|
|
15
|
+
|
|
16
|
+
### Running Tests
|
|
17
|
+
|
|
18
|
+
1) The test environment requires Stripe CLI package to be installed.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
curl -L https://github.com/stripe/stripe-cli/releases/download/v1.39.0/stripe_1.39.0_linux_x86_64.tar.gz -o stripe.tar.gz
|
|
22
|
+
sudo tar -xvf stripe.tar.gz -C /usr/local/bin
|
|
23
|
+
rm stripe.tar.gz
|
|
24
|
+
|
|
25
|
+
stripe --version
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2) Run test: `poetry run pytest -s`
|
|
30
|
+
|
|
31
|
+
To generate coverage report, use `poetry run pytest --cov --cov-branch --cov-report=xml`
|
|
32
|
+
|
|
33
|
+
Whenever tests fail, you can check `stripe_cli.log` for more details.
|
|
File without changes
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.http import HttpRequest
|
|
6
|
+
from django.utils.html import format_html
|
|
7
|
+
from django.utils.safestring import mark_safe
|
|
8
|
+
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
|
|
10
|
+
from django_stripe_hooks.models import (
|
|
11
|
+
Product, Price, PriceTier,
|
|
12
|
+
Coupon, PromotionCode,
|
|
13
|
+
Customer, PaymentMethod, Subscription,
|
|
14
|
+
PaymentIntent, BalanceTransaction, Invoice, Charge, Refund
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ParentModelT = TypeVar("ParentModelT", bound=models.Model)
|
|
19
|
+
ChildModelT = TypeVar("ChildModelT", bound=models.Model)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StripeModelAdmin(admin.ModelAdmin[ParentModelT]):
|
|
23
|
+
"""ModelAdmin for Stripe models.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
Authors are required to use Stripe SDK to create objects,
|
|
28
|
+
so we remove add/change/delete permissions here.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def has_add_permission(
|
|
33
|
+
self,
|
|
34
|
+
request: HttpRequest,
|
|
35
|
+
) -> bool:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def has_change_permission(
|
|
39
|
+
self,
|
|
40
|
+
request: HttpRequest,
|
|
41
|
+
obj: ParentModelT | None = None,
|
|
42
|
+
) -> bool:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def has_delete_permission(
|
|
46
|
+
self,
|
|
47
|
+
request: HttpRequest,
|
|
48
|
+
obj: ParentModelT | None = None,
|
|
49
|
+
) -> bool:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StripeModelInline(admin.TabularInline[ChildModelT, ParentModelT]):
|
|
54
|
+
"""Inline ModelAdmin for Stripe models.
|
|
55
|
+
|
|
56
|
+
Notes
|
|
57
|
+
-----
|
|
58
|
+
Authors are required to use Stripe SDK to create objects,
|
|
59
|
+
so we remove add/change/delete permissions here.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def has_add_permission(
|
|
64
|
+
self,
|
|
65
|
+
request: HttpRequest,
|
|
66
|
+
obj: models.Model | None = None,
|
|
67
|
+
) -> bool:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def has_change_permission(
|
|
71
|
+
self,
|
|
72
|
+
request: HttpRequest,
|
|
73
|
+
obj: models.Model | None = None,
|
|
74
|
+
) -> bool:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def has_delete_permission(
|
|
78
|
+
self,
|
|
79
|
+
request: HttpRequest,
|
|
80
|
+
obj: models.Model | None = None,
|
|
81
|
+
) -> bool:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@admin.register(Product)
|
|
86
|
+
class ProductAdmin(StripeModelAdmin[Product]):
|
|
87
|
+
list_display = (
|
|
88
|
+
'name',
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
fieldsets = (
|
|
92
|
+
(None, {
|
|
93
|
+
'fields': (
|
|
94
|
+
'name',
|
|
95
|
+
'description',
|
|
96
|
+
),
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PriceTierInline(StripeModelInline[PriceTier, Price]):
|
|
102
|
+
model = PriceTier
|
|
103
|
+
extra = 2
|
|
104
|
+
fields = ('flat_amount', 'unit_amount', 'up_to')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# TODO: This uses custom admin html and css. Is it needed?
|
|
108
|
+
@admin.register(Price)
|
|
109
|
+
class PriceAdmin(StripeModelAdmin[Price]):
|
|
110
|
+
list_display = (
|
|
111
|
+
'nickname',
|
|
112
|
+
'product',
|
|
113
|
+
'type',
|
|
114
|
+
'interval',
|
|
115
|
+
'unit_amount',
|
|
116
|
+
'active',
|
|
117
|
+
)
|
|
118
|
+
list_select_related = ('product', )
|
|
119
|
+
|
|
120
|
+
fieldsets = (
|
|
121
|
+
(None, {
|
|
122
|
+
'fields': (
|
|
123
|
+
'product',
|
|
124
|
+
'nickname',
|
|
125
|
+
'active',
|
|
126
|
+
),
|
|
127
|
+
}),
|
|
128
|
+
(_("Billing Details"), {
|
|
129
|
+
'fields': (
|
|
130
|
+
'unit_amount',
|
|
131
|
+
'type',
|
|
132
|
+
'interval',
|
|
133
|
+
'usage_type',
|
|
134
|
+
'billing_scheme',
|
|
135
|
+
'tiers_mode',
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@admin.register(Coupon)
|
|
142
|
+
class CouponAdmin(StripeModelAdmin[Coupon]):
|
|
143
|
+
list_display = (
|
|
144
|
+
'name',
|
|
145
|
+
'terms',
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
fieldsets = (
|
|
149
|
+
(None, {
|
|
150
|
+
'fields': (
|
|
151
|
+
'name',
|
|
152
|
+
'duration',
|
|
153
|
+
'percent_off',
|
|
154
|
+
'amount_off',
|
|
155
|
+
'products',
|
|
156
|
+
),
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
filter_horizontal = ('products', )
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@admin.register(PromotionCode)
|
|
163
|
+
class PromotionCodeAdmin(StripeModelAdmin[PromotionCode]):
|
|
164
|
+
list_display = (
|
|
165
|
+
'code',
|
|
166
|
+
'coupon',
|
|
167
|
+
'coupon__terms',
|
|
168
|
+
'expires_at',
|
|
169
|
+
'redemptions',
|
|
170
|
+
'active',
|
|
171
|
+
)
|
|
172
|
+
list_select_related = ('coupon', )
|
|
173
|
+
list_display_links = None
|
|
174
|
+
|
|
175
|
+
fieldsets = (
|
|
176
|
+
(None, {
|
|
177
|
+
'fields': (
|
|
178
|
+
'code',
|
|
179
|
+
'coupon',
|
|
180
|
+
'expires_at',
|
|
181
|
+
'max_redemptions',
|
|
182
|
+
'active',
|
|
183
|
+
),
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@admin.display(description=_("Terms"))
|
|
188
|
+
def coupon__terms(self, obj: PromotionCode) -> str:
|
|
189
|
+
return obj.coupon.terms
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SubscriptionInline(StripeModelInline[Subscription, Customer]):
|
|
193
|
+
model = Subscription
|
|
194
|
+
extra = 0
|
|
195
|
+
fields = (
|
|
196
|
+
'status',
|
|
197
|
+
'current_period_start',
|
|
198
|
+
'current_period_end',
|
|
199
|
+
'promotion_code',
|
|
200
|
+
'cancel_at_period_end',
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PaymentMethodInline(StripeModelInline[PaymentMethod, Customer]):
|
|
205
|
+
model = PaymentMethod
|
|
206
|
+
extra = 0
|
|
207
|
+
fields = (
|
|
208
|
+
'card_info',
|
|
209
|
+
'card_exp_month',
|
|
210
|
+
'card_exp_year',
|
|
211
|
+
'zip_code',
|
|
212
|
+
'is_default',
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class InvoiceInline(StripeModelInline[Invoice, Customer]):
|
|
217
|
+
model = Invoice
|
|
218
|
+
extra = 0
|
|
219
|
+
fields = (
|
|
220
|
+
'total',
|
|
221
|
+
'status',
|
|
222
|
+
'period_start',
|
|
223
|
+
'period_end',
|
|
224
|
+
'pdf',
|
|
225
|
+
'link',
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@admin.display(description=_("PDF"))
|
|
229
|
+
def pdf_link(self, obj: Invoice) -> str:
|
|
230
|
+
html = format_html(
|
|
231
|
+
'<a href="{url}">PDF</a>',
|
|
232
|
+
url=obj.invoice_pdf,
|
|
233
|
+
)
|
|
234
|
+
return html
|
|
235
|
+
|
|
236
|
+
@admin.display(description=_("Link"))
|
|
237
|
+
def link(self, obj: Invoice) -> str:
|
|
238
|
+
html = format_html(
|
|
239
|
+
'<a href="{url}">Link</a>',
|
|
240
|
+
url=obj.hosted_invoice_url,
|
|
241
|
+
)
|
|
242
|
+
return html
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@admin.register(Customer)
|
|
246
|
+
class CustomerAdmin(StripeModelAdmin[Customer]):
|
|
247
|
+
search_fields = (
|
|
248
|
+
'email',
|
|
249
|
+
'name',
|
|
250
|
+
'phone',
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
list_display = (
|
|
254
|
+
'email',
|
|
255
|
+
'name',
|
|
256
|
+
'phone',
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
fieldsets = (
|
|
260
|
+
(None, {'fields': ()}),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
inline_type = 'stacked'
|
|
264
|
+
inlines = (
|
|
265
|
+
SubscriptionInline,
|
|
266
|
+
PaymentMethodInline,
|
|
267
|
+
InvoiceInline,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@admin.register(Subscription)
|
|
272
|
+
class SubscriptionAdmin(StripeModelAdmin[Subscription]):
|
|
273
|
+
search_fields = (
|
|
274
|
+
'customer__email',
|
|
275
|
+
'customer__name',
|
|
276
|
+
'customer__phone',
|
|
277
|
+
)
|
|
278
|
+
list_display = (
|
|
279
|
+
'customer',
|
|
280
|
+
'status_verbose',
|
|
281
|
+
'promotion_code',
|
|
282
|
+
'current_period_end',
|
|
283
|
+
)
|
|
284
|
+
list_select_related = (
|
|
285
|
+
'customer',
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
fieldsets = (
|
|
289
|
+
(None, {
|
|
290
|
+
'fields': (
|
|
291
|
+
'customer',
|
|
292
|
+
'current_period_start',
|
|
293
|
+
'current_period_end',
|
|
294
|
+
'promotion_code',
|
|
295
|
+
'cancel_at_period_end',
|
|
296
|
+
),
|
|
297
|
+
}),
|
|
298
|
+
)
|
|
299
|
+
inlines = (InvoiceInline, )
|
|
300
|
+
|
|
301
|
+
@admin.display(description=_("Status"), ordering='status')
|
|
302
|
+
def status_verbose(self, obj: Subscription) -> str:
|
|
303
|
+
if obj.cancel_at_period_end:
|
|
304
|
+
status = 'cancels'
|
|
305
|
+
else:
|
|
306
|
+
status = obj.status
|
|
307
|
+
|
|
308
|
+
text = {
|
|
309
|
+
k: str(v) for k, v in Subscription.STATUSES
|
|
310
|
+
} | {
|
|
311
|
+
'active': f'Renews {obj.current_period_end:%b %d, %Y}',
|
|
312
|
+
'cancels': f'Cancels {obj.current_period_end:%b %d, %Y}',
|
|
313
|
+
}
|
|
314
|
+
return text[status]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@admin.register(PaymentIntent)
|
|
318
|
+
class PaymentIntentAdmin(StripeModelAdmin[PaymentIntent]):
|
|
319
|
+
search_fields = (
|
|
320
|
+
'amount',
|
|
321
|
+
'description',
|
|
322
|
+
'customer__email',
|
|
323
|
+
'customer__name',
|
|
324
|
+
'customer__phone',
|
|
325
|
+
)
|
|
326
|
+
list_display = (
|
|
327
|
+
'payment_method',
|
|
328
|
+
'amount',
|
|
329
|
+
'customer',
|
|
330
|
+
'description',
|
|
331
|
+
'status',
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@admin.register(BalanceTransaction)
|
|
336
|
+
class BalanceTransactionAdmin(StripeModelAdmin[BalanceTransaction]):
|
|
337
|
+
search_fields = (
|
|
338
|
+
'type',
|
|
339
|
+
'status',
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
list_display = (
|
|
343
|
+
'type',
|
|
344
|
+
'status',
|
|
345
|
+
'amount_display',
|
|
346
|
+
'fee_display',
|
|
347
|
+
'net_display',
|
|
348
|
+
'available_on',
|
|
349
|
+
)
|
|
350
|
+
list_display_links = None
|
|
351
|
+
|
|
352
|
+
def _currency_display(self, obj: BalanceTransaction, field: str) -> str:
|
|
353
|
+
"""Displays currency-related fields."""
|
|
354
|
+
amount = getattr(obj, field)
|
|
355
|
+
if field == 'fee' or amount < 0:
|
|
356
|
+
sign = '-'
|
|
357
|
+
elif amount > 0:
|
|
358
|
+
sign = '+'
|
|
359
|
+
elif amount == 0:
|
|
360
|
+
sign = ''
|
|
361
|
+
|
|
362
|
+
if obj.currency == 'usd':
|
|
363
|
+
s = f"{sign}${abs(amount)} USD"
|
|
364
|
+
else:
|
|
365
|
+
s = f"{sign} {abs(amount) * 100} {obj.currency.upper()}"
|
|
366
|
+
return s
|
|
367
|
+
|
|
368
|
+
@admin.display(description=_("Amount"), ordering='amount')
|
|
369
|
+
def amount_display(self, obj: BalanceTransaction) -> str:
|
|
370
|
+
return self._currency_display(obj, 'amount')
|
|
371
|
+
|
|
372
|
+
@admin.display(description=_("Fee"), ordering='fee')
|
|
373
|
+
def fee_display(self, obj: BalanceTransaction) -> str:
|
|
374
|
+
return self._currency_display(obj, 'fee')
|
|
375
|
+
|
|
376
|
+
@admin.display(description=_("Net"), ordering='net')
|
|
377
|
+
def net_display(self, obj: BalanceTransaction) -> str:
|
|
378
|
+
return self._currency_display(obj, 'net')
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@admin.register(Invoice)
|
|
382
|
+
class InvoiceAdmin(StripeModelAdmin[Invoice]):
|
|
383
|
+
search_fields = (
|
|
384
|
+
'customer__email',
|
|
385
|
+
'customer__name',
|
|
386
|
+
'customer__phone',
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
list_display = (
|
|
390
|
+
'customer',
|
|
391
|
+
'total',
|
|
392
|
+
'period_start',
|
|
393
|
+
'period_end',
|
|
394
|
+
'status_chip',
|
|
395
|
+
'pdf',
|
|
396
|
+
'link',
|
|
397
|
+
)
|
|
398
|
+
list_select_related = (
|
|
399
|
+
'customer',
|
|
400
|
+
'subscription',
|
|
401
|
+
)
|
|
402
|
+
list_filter = (
|
|
403
|
+
'period_start',
|
|
404
|
+
)
|
|
405
|
+
list_display_links = None
|
|
406
|
+
|
|
407
|
+
@admin.display(description=_("Status"), ordering='status')
|
|
408
|
+
def status_chip(self, obj: Invoice) -> str:
|
|
409
|
+
"""Format to add badge HTML."""
|
|
410
|
+
STATUSES = {
|
|
411
|
+
'draft': ('Draft', 'info'),
|
|
412
|
+
'open': ('Open', 'warn'),
|
|
413
|
+
'paid': ('Paid', 'ok'),
|
|
414
|
+
'uncollectible': ('Uncollectible', 'bad'),
|
|
415
|
+
'void': ('Void', 'bad'),
|
|
416
|
+
}
|
|
417
|
+
status, color = STATUSES[obj.status]
|
|
418
|
+
html = f'<chip class="{color}">{status}</chip>'
|
|
419
|
+
return mark_safe(html)
|
|
420
|
+
|
|
421
|
+
@admin.display(description=_("PDF"))
|
|
422
|
+
def pdf(self, obj: Invoice) -> str:
|
|
423
|
+
html = format_html(
|
|
424
|
+
'<a href="{url}">PDF</a>',
|
|
425
|
+
url=obj.invoice_pdf,
|
|
426
|
+
)
|
|
427
|
+
return html
|
|
428
|
+
|
|
429
|
+
@admin.display(description=_("Link"))
|
|
430
|
+
def link(self, obj: Invoice) -> str:
|
|
431
|
+
html = format_html(
|
|
432
|
+
'<a href="{url}">Link</a>',
|
|
433
|
+
url=obj.hosted_invoice_url,
|
|
434
|
+
)
|
|
435
|
+
return html
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@admin.register(Charge)
|
|
439
|
+
class ChargeAdmin(StripeModelAdmin[Charge]):
|
|
440
|
+
search_fields = (
|
|
441
|
+
'customer__email',
|
|
442
|
+
'customer__name',
|
|
443
|
+
'customer__phone',
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
list_display = (
|
|
447
|
+
'customer',
|
|
448
|
+
'invoice',
|
|
449
|
+
'amount',
|
|
450
|
+
'status',
|
|
451
|
+
'created',
|
|
452
|
+
'description',
|
|
453
|
+
'disputed',
|
|
454
|
+
'refunded',
|
|
455
|
+
)
|
|
456
|
+
list_select_related = (
|
|
457
|
+
'customer',
|
|
458
|
+
'invoice',
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@admin.register(Refund)
|
|
463
|
+
class RefundAdmin(StripeModelAdmin[Refund]):
|
|
464
|
+
list_display = (
|
|
465
|
+
'charge__customer',
|
|
466
|
+
'amount',
|
|
467
|
+
'reason',
|
|
468
|
+
'status',
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
fieldsets = (
|
|
472
|
+
(None, {
|
|
473
|
+
'fields': (
|
|
474
|
+
'charge',
|
|
475
|
+
'reason',
|
|
476
|
+
('amount', 'currency'),
|
|
477
|
+
),
|
|
478
|
+
}),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
@admin.display(description=_("Customer"))
|
|
482
|
+
def charge__customer(self, obj: Refund) -> str:
|
|
483
|
+
assert isinstance(obj.charge, Charge)
|
|
484
|
+
return str(obj.charge.customer)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StripeConfig(AppConfig):
|
|
8
|
+
name = 'django_stripe_hooks'
|
|
9
|
+
verbose_name = _('Stripe Payments')
|
|
10
|
+
required_settings = [
|
|
11
|
+
'STRIPE_PUBLIC_KEY',
|
|
12
|
+
'STRIPE_SECRET_KEY',
|
|
13
|
+
'STRIPE_WEBHOOK_SECRET_KEY',
|
|
14
|
+
'STRIPE_API_VERSION',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def ready(self) -> None:
|
|
18
|
+
"""Validate that necessary settings have been defined."""
|
|
19
|
+
for value in self.required_settings:
|
|
20
|
+
if not hasattr(settings, value):
|
|
21
|
+
message = _(
|
|
22
|
+
f"[django-stripe-hooks] {value} must be defined in settings.py."
|
|
23
|
+
)
|
|
24
|
+
raise ImproperlyConfigured(message)
|