forked from dj-stripe/dj-stripe
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstripe_objects.py
1929 lines (1596 loc) · 83.2 KB
/
stripe_objects.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
.. module:: djstripe.stripe_objects
:synopsis: dj-stripe - Abstract model definitions to provide our view of Stripe's objects
.. moduleauthor:: Bill Huneke (@wahuneke)
.. moduleauthor:: Alex Kavanaugh (@kavdev)
.. moduleauthor:: Lee Skillen (@lskillen)
This module is an effort to isolate (as much as possible) the API dependent code in one
place. Primarily this is:
1) create models containing the fields that we care about, mapping to Stripe's fields
2) create methods for consistently syncing our database with Stripe's version of the objects
3) centralized methods for creating new database records to match incoming Stripe objects
This module defines abstract models which are then extended in models.py to provide the remaining
dj-stripe functionality.
"""
from copy import deepcopy
import decimal
import sys
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import dateformat, six, timezone
from django.utils.encoding import python_2_unicode_compatible, smart_text
from polymorphic.models import PolymorphicModel
import stripe
from stripe.error import InvalidRequestError
from . import enums, settings as djstripe_settings
from .context_managers import stripe_temporary_api_version
from .exceptions import StripeObjectManipulationException
from .fields import (
StripeBooleanField, StripeCharField, StripeCurrencyField, StripeDateTimeField,
StripeFieldMixin, StripeIdField, StripeIntegerField, StripeJSONField,
StripeNullBooleanField, StripePercentField, StripePositiveIntegerField,
StripeTextField
)
from .managers import StripeObjectManager
# Override the default API version used by the Stripe library.
djstripe_settings.set_stripe_api_version()
# ============================================================================ #
# Stripe Object Base #
# ============================================================================ #
@python_2_unicode_compatible
class StripeObject(models.Model):
# This must be defined in descendants of this model/mixin
# e.g. Event, Charge, Customer, etc.
stripe_class = None
expand_fields = None
stripe_dashboard_item_name = ""
objects = models.Manager()
stripe_objects = StripeObjectManager()
stripe_id = StripeIdField(unique=True, stripe_name='id')
livemode = StripeNullBooleanField(
default=None,
null=True,
stripe_required=False,
help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, "
"this field indicates whether this record comes from Stripe test mode or live mode operation."
)
stripe_timestamp = StripeDateTimeField(
null=True,
stripe_required=False,
stripe_name="created",
help_text="The datetime this object was created in stripe."
)
metadata = StripeJSONField(
blank=True,
stripe_required=False,
help_text="A set of key/value pairs that you can attach to an object. It can be useful for storing additional "
"information about an object in a structured format."
)
description = StripeTextField(blank=True, stripe_required=False, help_text="A description of this object.")
created = models.DateTimeField(auto_now_add=True, editable=False)
modified = models.DateTimeField(auto_now=True, editable=False)
class Meta:
abstract = True
def get_stripe_dashboard_url(self):
"""Get the stripe dashboard url for this object."""
base_url = "https://dashboard.stripe.com/"
if not self.livemode:
base_url += "test/"
if not self.stripe_dashboard_item_name or not self.stripe_id:
return ""
else:
return "{base_url}{item}/{stripe_id}".format(
base_url=base_url,
item=self.stripe_dashboard_item_name,
stripe_id=self.stripe_id
)
@property
def default_api_key(self):
if self.livemode is None:
# Livemode is unknown. Use the default secret key.
return djstripe_settings.STRIPE_SECRET_KEY
elif self.livemode:
# Livemode is true, use the live secret key
return djstripe_settings.LIVE_API_KEY or djstripe_settings.STRIPE_SECRET_KEY
else:
# Livemode is false, use the test secret key
return djstripe_settings.TEST_API_KEY or djstripe_settings.STRIPE_SECRET_KEY
def api_retrieve(self, api_key=None, stripe_account=None):
"""
Call the stripe API's retrieve operation for this model.
:param api_key: The api key to use for this request. Defaults to settings.STRIPE_SECRET_KEY.
:type api_key: string
"""
api_key = api_key or self.default_api_key
return self.stripe_class.retrieve(id=self.stripe_id, api_key=api_key, expand=self.expand_fields, stripe_account=stripe_account)
@classmethod
def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
"""
Call the stripe API's list operation for this model.
:param api_key: The api key to use for this request. Defualts to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
See Stripe documentation for accepted kwargs for each object.
:returns: an iterator over all items in the query
"""
return cls.stripe_class.list(api_key=api_key, **kwargs).auto_paging_iter()
@classmethod
def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
"""
Call the stripe API's create operation for this model.
:param api_key: The api key to use for this request. Defualts to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
"""
return cls.stripe_class.create(api_key=api_key, **kwargs)
def _api_delete(self, api_key=None, **kwargs):
"""
Call the stripe API's delete operation for this model
:param api_key: The api key to use for this request. Defualts to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
"""
api_key = api_key or self.default_api_key
return self.api_retrieve(api_key=api_key).delete(**kwargs)
def str_parts(self):
"""
Extend this to add information to the string representation of the object
:rtype: list of str
"""
return ["stripe_id={id}".format(id=self.stripe_id)]
@classmethod
def _manipulate_stripe_object_hook(cls, data):
"""
Gets called by this object's stripe object conversion method just before conversion.
Use this to populate custom fields in a StripeObject from stripe data.
"""
return data
@classmethod
def _stripe_object_to_record(cls, data):
"""
This takes an object, as it is formatted in Stripe's current API for our object
type. In return, it provides a dict. The dict can be used to create a record or
to update a record
This function takes care of mapping from one field name to another, converting
from cents to dollars, converting timestamps, and eliminating unused fields
(so that an objects.create() call would not fail).
:param data: the object, as sent by Stripe. Parsed from JSON, into a dict
:type data: dict
:return: All the members from the input, translated, mutated, etc
:rtype: dict
"""
manipulated_data = cls._manipulate_stripe_object_hook(data)
result = dict()
# Iterate over all the fields that we know are related to Stripe, let each field work its own magic
for field in filter(lambda x: isinstance(x, StripeFieldMixin), cls._meta.fields):
result[field.name] = field.stripe_to_db(manipulated_data)
return result
def _attach_objects_hook(self, cls, data):
"""
Gets called by this object's create and sync methods just before save.
Use this to populate fields before the model is saved.
:param cls: The target class for the instantiated object.
:param data: The data dictionary received from the Stripe API.
:type data: dict
"""
pass
def _attach_objects_post_save_hook(self, cls, data):
"""
Gets called by this object's create and sync methods just after save.
Use this to populate fields after the model is saved.
:param cls: The target class for the instantiated object.
:param data: The data dictionary received from the Stripe API.
:type data: dict
"""
pass
@classmethod
def _create_from_stripe_object(cls, data, save=True):
"""
Instantiates a model instance using the provided data object received
from Stripe, and saves it to the database if specified.
:param data: The data dictionary received from the Stripe API.
:type data: dict
:param save: If True, the object is saved after instantiation.
:type save: bool
:returns: The instantiated object.
"""
instance = cls(**cls._stripe_object_to_record(data))
instance._attach_objects_hook(cls, data)
if save:
instance.save()
instance._attach_objects_post_save_hook(cls, data)
return instance
@classmethod
def _get_or_create_from_stripe_object(cls, data, field_name="id", refetch=True, save=True):
field = data.get(field_name)
is_nested_data = field_name != "id"
should_expand = False
if isinstance(field, six.string_types):
# A field like {"subscription": "sub_6lsC8pt7IcFpjA", ...}
stripe_id = field
# We'll have to expand if the field is not "id" (= is nested)
should_expand = is_nested_data
elif field:
# A field like {"subscription": {"id": sub_6lsC8pt7IcFpjA", ...}}
data = field
stripe_id = field.get("id")
else:
# An empty field - We need to return nothing here because there is
# no way of knowing what needs to be fetched!
return None, False
try:
return cls.stripe_objects.get(stripe_id=stripe_id), False
except cls.DoesNotExist:
if is_nested_data and refetch:
# This is what `data` usually looks like:
# {"id": "cus_XXXX", "default_source": "card_XXXX"}
# Leaving the default field_name ("id") will get_or_create the customer.
# If field_name="default_source", we get_or_create the card instead.
cls_instance = cls(stripe_id=stripe_id)
data = cls_instance.api_retrieve()
should_expand = False
# The next thing to happen will be the "create from stripe object" call.
# At this point, if we don't have data to start with (field is a str),
# *and* we didn't refetch by id, then `should_expand` is True and we
# don't have the data to actually create the object.
# If this happens when syncing Stripe data, it's a djstripe bug. Report it!
assert not should_expand, "No data to create {} from {}".format(cls.__name__, field_name)
return cls._create_from_stripe_object(data, save=save), True
@classmethod
def _stripe_object_to_customer(cls, target_cls, data):
"""
Search the given manager for the Customer matching this object's ``customer`` field.
:param target_cls: The target class
:type target_cls: StripeCustomer
:param data: stripe object
:type data: dict
"""
if "customer" in data and data["customer"]:
return target_cls._get_or_create_from_stripe_object(data, "customer")[0]
@classmethod
def _stripe_object_to_transfer(cls, target_cls, data):
"""
Search the given manager for the Transfer matching this StripeCharge object's ``transfer`` field.
:param target_cls: The target class
:type target_cls: StripeTransfer
:param data: stripe object
:type data: dict
"""
if "transfer" in data and data["transfer"]:
return target_cls._get_or_create_from_stripe_object(data, "transfer")[0]
@classmethod
def _stripe_object_to_source(cls, target_cls, data):
"""
Search the given manager for the source matching this object's ``source`` field.
Note that the source field is already expanded in each request, and that it is required.
:param target_cls: The target class
:type target_cls: StripeSource
:param data: stripe object
:type data: dict
"""
return target_cls._get_or_create_from_stripe_object(data["source"])[0]
@classmethod
def _stripe_object_to_invoice(cls, target_cls, data):
"""
Search the given manager for the Invoice matching this StripeCharge object's ``invoice`` field.
Note that the invoice field is required.
Note that the invoice field is required.
:param target_cls: The target class
:type target_cls: StripeInvoice
:param data: stripe object
:type data: dict
"""
return target_cls._get_or_create_from_stripe_object(data, "invoice")[0]
@classmethod
def _stripe_object_to_invoice_items(cls, target_cls, data, invoice):
"""
Retrieves InvoiceItems for an invoice.
If the invoice item doesn't exist already then it is created.
If the invoice is an upcoming invoice that doesn't persist to the
database (i.e. ephemeral) then the invoice items are also not saved.
:param target_cls: The target class to instantiate per invoice item.
:type target_cls: ``StripeInvoiceItem``
:param data: The data dictionary received from the Stripe API.
:type data: dict
:param invoice: The invoice object that should hold the invoice items.
:type invoice: ``djstripe.models.Invoice``
"""
lines = data.get("lines")
if not lines:
return []
invoiceitems = []
for line in lines.get("data", []):
if invoice.stripe_id:
save = True
line.setdefault("invoice", invoice.stripe_id)
if line.get("type") == "subscription":
# Lines for subscriptions need to be keyed based on invoice and
# subscription, because their id is *just* the subscription
# when received from Stripe. This means that future updates to
# a subscription will change previously saved invoices - Doing
# the composite key avoids this.
if not line["id"].startswith(invoice.stripe_id):
line["id"] = "{invoice_id}-{subscription_id}".format(
invoice_id=invoice.stripe_id,
subscription_id=line["id"])
else:
# Don't save invoice items for ephemeral invoices
save = False
line.setdefault("customer", invoice.customer.stripe_id)
line.setdefault("date", int(dateformat.format(invoice.date, 'U')))
item, _ = target_cls._get_or_create_from_stripe_object(
line, refetch=False, save=save)
invoiceitems.append(item)
return invoiceitems
@classmethod
def _stripe_object_to_subscription(cls, target_cls, data):
"""
Search the given manager for the Subscription matching this object's ``subscription`` field.
:param target_cls: The target class
:type target_cls: StripeSubscription
:param data: stripe object
:type data: dict
"""
if "subscription" in data and data["subscription"]:
return target_cls._get_or_create_from_stripe_object(data, "subscription")[0]
def _sync(self, record_data):
for attr, value in record_data.items():
setattr(self, attr, value)
@classmethod
def sync_from_stripe_data(cls, data):
"""
Syncs this object from the stripe data provided.
:param data: stripe object
:type data: dict
"""
instance, created = cls._get_or_create_from_stripe_object(data)
if not created:
instance._sync(cls._stripe_object_to_record(data))
instance._attach_objects_hook(cls, data)
instance.save()
instance._attach_objects_post_save_hook(cls, data)
return instance
def __str__(self):
return smart_text("<{list}>".format(list=", ".join(self.str_parts())))
class StripeSource(PolymorphicModel, StripeObject):
customer = models.ForeignKey("Customer", on_delete=models.CASCADE, related_name="sources")
# ============================================================================ #
# Core Resources #
# ============================================================================ #
class StripeCharge(StripeObject):
"""
To charge a credit or a debit card, you create a charge object. You can
retrieve and refund individual charges as well as list all charges. Charges
are identified by a unique random ID. (Source: https://stripe.com/docs/api/python#charges)
# = Mapping the values of this field isn't currently on our roadmap.
Please use the stripe dashboard to check the value of this field instead.
Fields not implemented:
* **object** - Unnecessary. Just check the model name.
* **application_fee** - #. Coming soon with stripe connect functionality
* **balance_transaction** - #
* **dispute** - #; Mapped to a ``disputed`` boolean.
* **fraud_details** - Mapped to a ``fraudulent`` boolean.
* **order** - #
* **receipt_email** - Unnecessary. Use Customer.email. Create a feature request if this is functionality you need.
* **receipt_number** - Unnecessary. Use the dashboard. Create a feature request if this is functionality you need.
* **refunds** - #
* **source_transfer** - #
.. attention:: Stripe API_VERSION: model fields and methods audited to 2016-03-07 - @kavdev
"""
class Meta:
abstract = True
stripe_class = stripe.Charge
expand_fields = ["balance_transaction"]
stripe_dashboard_item_name = "payments"
amount = StripeCurrencyField(help_text="Amount charged.")
amount_refunded = StripeCurrencyField(
help_text="Amount refunded (can be less than the amount attribute on the charge "
"if a partial refund was issued)."
)
captured = StripeBooleanField(
default=False,
help_text="If the charge was created without capturing, this boolean represents whether or not it is still "
"uncaptured or has since been captured."
)
currency = StripeCharField(
max_length=3,
help_text="Three-letter ISO currency code representing the currency in which the charge was made."
)
failure_code = StripeCharField(
max_length=30,
null=True,
choices=enums.ApiErrorCode.choices,
help_text="Error code explaining reason for charge failure if available."
)
failure_message = StripeTextField(
null=True,
help_text="Message to user further explaining reason for charge failure if available."
)
paid = StripeBooleanField(
default=False,
help_text="True if the charge succeeded, or was successfully authorized for later capture, False otherwise."
)
refunded = StripeBooleanField(
default=False,
help_text="Whether or not the charge has been fully refunded. If the charge is only partially refunded, "
"this attribute will still be false."
)
shipping = StripeJSONField(null=True, help_text="Shipping information for the charge")
statement_descriptor = StripeCharField(
max_length=22, null=True,
help_text="An arbitrary string to be displayed on your customer's credit card statement. The statement "
"description may not include <>\"' characters, and will appear on your customer's statement in capital "
"letters. Non-ASCII characters are automatically stripped. While most banks display this information "
"consistently, some may display it incorrectly or not at all."
)
status = StripeCharField(
max_length=10, choices=enums.ChargeStatus.choices, help_text="The status of the payment."
)
# Balance transaction can be null if the charge failed
fee = StripeCurrencyField(stripe_required=False, nested_name="balance_transaction")
fee_details = StripeJSONField(stripe_required=False, nested_name="balance_transaction")
# dj-stripe custom stripe fields. Don't try to send these.
source_type = StripeCharField(
max_length=20,
null=True,
choices=enums.SourceType.choices,
stripe_name="source.object",
help_text="The payment source type. If the payment source is supported by dj-stripe, a corresponding model is "
"attached to this Charge via a foreign key matching this field."
)
source_stripe_id = StripeIdField(null=True, stripe_name="source.id", help_text="The payment source id.")
disputed = StripeBooleanField(default=False, help_text="Whether or not this charge is disputed.")
fraudulent = StripeBooleanField(default=False, help_text="Whether or not this charge was marked as fraudulent.")
def str_parts(self):
return [
"amount={amount}".format(amount=self.amount),
"paid={paid}".format(paid=smart_text(self.paid)),
] + super(StripeCharge, self).str_parts()
def _calculate_refund_amount(self, amount=None):
"""
:rtype: int
:return: amount that can be refunded, in CENTS
"""
eligible_to_refund = self.amount - (self.amount_refunded or 0)
if amount:
amount_to_refund = min(eligible_to_refund, amount)
else:
amount_to_refund = eligible_to_refund
return int(amount_to_refund * 100)
def refund(self, amount=None, reason=None):
"""
Initiate a refund. If amount is not provided, then this will be a full refund.
:param amount: A positive decimal amount representing how much of this charge
to refund. Can only refund up to the unrefunded amount remaining of the charge.
:trye amount: Decimal
:param reason: String indicating the reason for the refund. If set, possible values
are ``duplicate``, ``fraudulent``, and ``requested_by_customer``. Specifying
``fraudulent`` as the reason when you believe the charge to be fraudulent will
help Stripe improve their fraud detection algorithms.
:return: Stripe charge object
:rtype: dict
"""
charge_obj = self.api_retrieve().refund(
amount=self._calculate_refund_amount(amount=amount),
reason=reason,
)
return charge_obj
def capture(self):
"""
Capture the payment of an existing, uncaptured, charge. This is the second half of the two-step payment flow,
where first you created a charge with the capture option set to false.
See https://stripe.com/docs/api#capture_charge
"""
return self.api_retrieve().capture()
@classmethod
def _stripe_object_destination_to_account(cls, target_cls, data):
"""
Search the given manager for the Account matching this StripeCharge object's ``destination`` field.
:param target_cls: The target class
:type target_cls: StripeAccount
:param data: stripe object
:type data: dict
"""
if "destination" in data and data["destination"]:
return target_cls._get_or_create_from_stripe_object(data, "destination")[0]
@classmethod
def _manipulate_stripe_object_hook(cls, data):
data["disputed"] = data["dispute"] is not None
# Assessments reported by you have the key user_report and, if set,
# possible values of safe and fraudulent. Assessments from Stripe have
# the key stripe_report and, if set, the value fraudulent.
data["fraudulent"] = bool(data["fraud_details"]) and list(data["fraud_details"].values())[0] == "fraudulent"
return data
class StripeCustomer(StripeObject):
"""
Customer objects allow you to perform recurring charges and track multiple charges that are
associated with the same customer. (Source: https://stripe.com/docs/api/python#customers)
# = Mapping the values of this field isn't currently on our roadmap.
Please use the stripe dashboard to check the value of this field instead.
Fields not implemented:
* **object** - Unnecessary. Just check the model name.
* **discount** - #
* **email** - Unnecessary. Use ``Customer.subscriber.email``.
.. attention:: Stripe API_VERSION: model fields and methods audited to 2016-03-07 - @kavdev
"""
class Meta:
abstract = True
stripe_class = stripe.Customer
expand_fields = ["default_source"]
stripe_dashboard_item_name = "customers"
account_balance = StripeIntegerField(
null=True,
help_text="Current balance, if any, being stored on the customer's account. If negative, the customer has "
"credit to apply to the next invoice. If positive, the customer has an amount owed that will be added to the "
"next invoice. The balance does not refer to any unpaid invoices; it solely takes into account amounts that "
"have yet to be successfully applied to any invoice. This balance is only taken into account for recurring "
"charges."
)
business_vat_id = StripeCharField(
max_length=20,
null=True,
stripe_required=False,
help_text="The customer's VAT identification number.",
)
currency = StripeCharField(
max_length=3,
null=True,
help_text="The currency the customer can be charged in for recurring billing purposes (subscriptions, "
"invoices, invoice items)."
)
delinquent = StripeBooleanField(
default=False,
help_text="Whether or not the latest charge for the customer's latest invoice has failed."
)
shipping = StripeJSONField(null=True, help_text="Shipping information associated with the customer.")
def subscribe(self, plan, application_fee_percent=None, coupon=None, quantity=None, metadata=None,
tax_percent=None, trial_end=None):
"""
Subscribes this customer to a plan.
Parameters not implemented:
* **source** - Subscriptions use the customer's default source. Including the source parameter creates \
a new source for this customer and overrides the default source. This functionality is not \
desired; add a source to the customer before attempting to add a subscription. \
:param plan: The plan to which to subscribe the customer.
:type plan: Plan or string (plan ID)
:param application_fee_percent: This represents the percentage of the subscription invoice subtotal
that will be transferred to the application owner's Stripe account.
The request must be made with an OAuth key in order to set an
application fee percentage.
:type application_fee_percent: Decimal. Precision is 2; anything more will be ignored. A positive
decimal between 1 and 100.
:param coupon: The code of the coupon to apply to this subscription. A coupon applied to a subscription
will only affect invoices created for that particular subscription.
:type coupon: string
:param quantity: The quantity applied to this subscription. Default is 1.
:type quantity: integer
:param metadata: A set of key/value pairs useful for storing additional information.
:type metadata: dict
:param tax_percent: This represents the percentage of the subscription invoice subtotal that will
be calculated and added as tax to the final amount each billing period.
:type tax_percent: Decimal. Precision is 2; anything more will be ignored. A positive decimal
between 1 and 100.
:param trial_end: The end datetime of the trial period the customer will get before being charged for
the first time. If set, this will override the default trial period of the plan the
customer is being subscribed to. The special value ``now`` can be provided to end
the customer's trial immediately.
:type trial_end: datetime
:param charge_immediately: Whether or not to charge for the subscription upon creation. If False, an
invoice will be created at the end of this period.
:type charge_immediately: boolean
.. Notes:
.. ``charge_immediately`` is only available on ``Customer.subscribe()``
.. if you're using ``StripeCustomer.subscribe()`` instead of ``Customer.subscribe()``, ``plan`` \
can only be a string
"""
stripe_subscription = StripeSubscription._api_create(
plan=plan,
customer=self.stripe_id,
application_fee_percent=application_fee_percent,
coupon=coupon,
quantity=quantity,
metadata=metadata,
tax_percent=tax_percent,
trial_end=trial_end,
)
return stripe_subscription
def charge(self, amount, currency, application_fee=None, capture=None, description=None, destination=None,
metadata=None, shipping=None, source=None, statement_descriptor=None, stripe_account=None,
idempotency_key=None):
# TK added stripe_account=None
"""
Creates a charge for this customer.
Parameters not implemented:
* **receipt_email** - Since this is a charge on a customer, the customer's email address is used.
:param amount: The amount to charge.
:type amount: Decimal. Precision is 2; anything more will be ignored.
:param currency: 3-letter ISO code for currency
:type currency: string
:param application_fee: A fee that will be applied to the charge and transfered to the platform owner's
account.
:type application_fee: Decimal. Precision is 2; anything more will be ignored.
:param capture: Whether or not to immediately capture the charge. When false, the charge issues an
authorization (or pre-authorization), and will need to be captured later. Uncaptured
charges expire in 7 days. Default is True
:type capture: bool
:param description: An arbitrary string.
:type description: string
:param destination: An account to make the charge on behalf of.
:type destination: Account
:param metadata: A set of key/value pairs useful for storing additional information.
:type metadata: dict
:param shipping: Shipping information for the charge.
:type shipping: dict
:param source: The source to use for this charge. Must be a source attributed to this customer. If None,
the customer's default source is used. Can be either the id of the source or the source object
itself.
:type source: string, StripeSource
:param statement_descriptor: An arbitrary string to be displayed on the customer's credit card statement.
:type statement_descriptor: string
"""
if not isinstance(amount, decimal.Decimal):
raise ValueError("You must supply a decimal value representing dollars.")
# Convert StripeSource to stripe_id
if source and isinstance(source, StripeSource):
source = source.stripe_id
stripe_charge = StripeCharge._api_create(
amount=int(amount * 100), # Convert dollars into cents
currency=currency,
application_fee=int(application_fee * 100) if application_fee else None, # Convert dollars into cents
capture=capture,
description=description,
destination=destination,
metadata=metadata,
shipping=shipping,
customer=self.stripe_id,
source=source,
statement_descriptor=statement_descriptor,
stripe_account=stripe_account,
idempotency_key=idempotency_key
)
return stripe_charge
def add_invoice_item(self, amount, currency, description=None, discountable=None, invoice=None,
metadata=None, subscription=None):
"""
Adds an arbitrary charge or credit to the customer's upcoming invoice.
Different than creating a charge. Charges are separate bills that get
processed immediately. Invoice items are appended to the customer's next
invoice. This is extremely useful when adding surcharges to subscriptions.
:param amount: The amount to charge.
:type amount: Decimal. Precision is 2; anything more will be ignored.
:param currency: 3-letter ISO code for currency
:type currency: string
:param description: An arbitrary string.
:type description: string
:param discountable: Controls whether discounts apply to this invoice item. Defaults to False for
prorations or negative invoice items, and True for all other invoice items.
:type discountable: boolean
:param invoice: An existing invoice to add this invoice item to. When left blank, the invoice
item will be added to the next upcoming scheduled invoice. Use this when adding
invoice items in response to an ``invoice.created`` webhook. You cannot add an invoice
item to an invoice that has already been paid, attempted or closed.
:type invoice: Invoice or string (invoice ID)
:param metadata: A set of key/value pairs useful for storing additional information.
:type metadata: dict
:param subscription: A subscription to add this invoice item to. When left blank, the invoice
item will be be added to the next upcoming scheduled invoice. When set,
scheduled invoices for subscriptions other than the specified subscription
will ignore the invoice item. Use this when you want to express that an
invoice item has been accrued within the context of a particular subscription.
:type subscription: Subscription or string (subscription ID)
.. Notes:
.. if you're using ``StripeCustomer.add_invoice_item()`` instead of ``Customer.add_invoice_item()``, \
``invoice`` and ``subscriptions`` can only be strings
"""
if not isinstance(amount, decimal.Decimal):
raise ValueError("You must supply a decimal value representing dollars.")
stripe_invoiceitem = StripeInvoiceItem._api_create(
amount=int(amount * 100), # Convert dollars into cents
currency=currency,
customer=self.stripe_id,
description=description,
discountable=discountable,
invoice=invoice,
metadata=metadata,
subscription=subscription,
)
return stripe_invoiceitem
def add_card(self, source, set_default=True, stripe_account=None):
"""
Adds a card to this customer's account.
:param source: Either a token, like the ones returned by our Stripe.js, or a dictionary containing a
user's credit card details. Stripe will automatically validate the card.
:type source: string, dict
:param set_default: Whether or not to set the source as the customer's default source
:type set_default: boolean
"""
stripe_customer = self.api_retrieve(stripe_account=stripe_account)
stripe_card = stripe_customer.sources.create(source=source)
if set_default:
stripe_customer.default_source = stripe_card["id"]
stripe_customer.save()
return stripe_card
class StripeEvent(StripeObject):
"""
Events are POSTed to our webhook url. They provide information about a Stripe event that just happened. Events
are processed in detail by their respective models (charge events by the Charge model, etc).
Events are initially **UNTRUSTED**, as it is possible for any web entity to post any data to our webhook url. Data
posted may be valid Stripe information, garbage, or even malicious. The 'valid' flag in this model monitors this.
**API VERSIONING**
This is a tricky matter when it comes to webhooks. See the discussion here_.
.. _here: https://groups.google.com/a/lists.stripe.com/forum/#!topic/api-discuss/h5Y6gzNBZp8
In this discussion, it is noted that Webhooks are produced in one API version, which will usually be
different from the version supported by Stripe plugins (such as djstripe). The solution, described there,
is:
1) validate the receipt of a webhook event by doing an event get using the API version of the received hook event.
2) retrieve the referenced object (e.g. the Charge, the Customer, etc) using the plugin's supported API version.
3) process that event using the retrieved object which will, only now, be in a format that you are certain to \
understand
# = Mapping the values of this field isn't currently on our roadmap.
Please use the stripe dashboard to check the value of this field instead.
Fields not implemented:
* **object** - Unnecessary. Just check the model name.
* **pending_webhooks** - Unnecessary. Use the dashboard.
.. attention:: Stripe API_VERSION: model fields and methods audited to 2016-03-07 - @kavdev
"""
class Meta:
abstract = True
stripe_class = stripe.Event
stripe_dashboard_item_name = "events"
type = StripeCharField(max_length=250, help_text="Stripe's event description code")
request_id = StripeCharField(
max_length=50,
null=True,
blank=True,
stripe_name="request",
help_text="Information about the request that triggered this event, for traceability purposes. If empty "
"string then this is an old entry without that data. If Null then this is not an old entry, but a Stripe "
"'automated' event with no associated request."
)
received_api_version = StripeCharField(
max_length=15, blank=True, stripe_name="api_version", help_text="the API version at which the event data was "
"rendered. Blank for old entries only, all new entries will have this value"
)
webhook_message = StripeJSONField(
stripe_name="data",
help_text="data received at webhook. data should be considered to be garbage until validity check is run "
"and valid flag is set"
)
def str_parts(self):
return [
"type={type}".format(type=self.type),
] + super(StripeEvent, self).str_parts()
def api_retrieve(self, api_key=None):
# OVERRIDING the parent version of this function
# Event retrieve is special. For Event we don't retrieve using djstripe's API version. We always retrieve
# using the API version that was used to send the Event (which depends on the Stripe account holders settings
api_key = api_key or self.default_api_key
api_version = self.received_api_version
# Stripe API version validation is bypassed because we assume what
# Stripe passes us is a sane and usable value.
with stripe_temporary_api_version(api_version, validate=False):
# Determine if this is a Stripe Connect event
stripe_account = self.webhook_message.get('account', None)
stripe_event = super(StripeEvent, self).api_retrieve(api_key, stripe_account)
return stripe_event
class StripeTransfer(StripeObject):
"""
When Stripe sends you money or you initiate a transfer to a bank account, debit card, or
connected Stripe account, a transfer object will be created.
(Source: https://stripe.com/docs/api/python#transfers)
# = Mapping the values of this field isn't currently on our roadmap.
Please use the stripe dashboard to check the value of this field instead.
Fields not implemented:
* **object** - Unnecessary. Just check the model name.
* **application_fee** - #
* **balance_transaction** - #
* **reversals** - #
.. TODO: Link destination to Card, Account, or Bank Account Models
.. attention:: Stripe API_VERSION: model fields and methods audited to 2016-03-07 - @kavdev
"""
class Meta:
abstract = True
stripe_class = stripe.Transfer
expand_fields = ["balance_transaction"]
stripe_dashboard_item_name = "transfers"
# The following accessors are deprecated as of 1.0 and will be removed in 1.1
# Please use enums.SubscriptionStatus directly.
STATUS_PAID = enums.PayoutStatus.paid
STATUS_PENDING = enums.PayoutStatus.pending
STATUS_IN_TRANSIT = enums.PayoutStatus.in_transit
STATUS_CANCELED = enums.PayoutStatus.canceled
STATUS_CANCELLED = STATUS_CANCELED
STATUS_FAILED = enums.PayoutStatus.failed
DESTINATION_TYPES = ["card", "bank_account", "stripe_account"]
DESITNATION_TYPE_CHOICES = [
(destination_type, destination_type.replace("_", " ").title()) for destination_type in DESTINATION_TYPES
]
amount = StripeCurrencyField(help_text="The amount transferred")
amount_reversed = StripeCurrencyField(
stripe_required=False,
help_text="The amount reversed (can be less than the amount attribute on the transfer if a partial "
"reversal was issued)."