-
-
Notifications
You must be signed in to change notification settings - Fork 493
/
Copy pathbase.py
1148 lines (986 loc) · 41.9 KB
/
base.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
import logging
import uuid
from datetime import timedelta
from typing import Dict, List, Optional, Type
from django.db import IntegrityError, models, transaction
from django.utils import dateformat, timezone
from stripe import (
APIResource,
InvalidRequestError,
StripeObject,
convert_to_stripe_object,
)
from ..exceptions import ImpossibleAPIRequest
from ..fields import (
JSONField,
StripeDateTimeField,
StripeForeignKey,
StripeIdField,
StripePercentField,
)
from ..managers import StripeModelManager
from ..settings import djstripe_settings
from ..utils import get_id_from_stripe_data
logger = logging.getLogger(__name__)
class StripeBaseModel(models.Model):
stripe_class: Type[APIResource] = APIResource
djstripe_created = models.DateTimeField(auto_now_add=True, editable=False)
djstripe_updated = models.DateTimeField(auto_now=True, editable=False)
stripe_data = JSONField(default=dict)
class Meta:
abstract = True
@classmethod
def get_expand_params(cls, api_key, **kwargs):
"""Populate `expand` kwarg in stripe api calls by updating the kwargs passed."""
# To avoid Circular Import Error
from djstripe.management.commands.djstripe_sync_models import Command
# As api_list is a class method we will never get the stripe account unless we
# default to the owner account of the api_key. But even that is pointless as we only care about expand
# So no need to make a call to Stripe and again do an account object sync which would make
# no sense if this is for a Stripe Connected Account
expand = Command.get_default_list_kwargs(
cls, {kwargs.get("stripe_account", "acct_fake")}, api_key
)[0].get("expand", [])
# Add expand to the provided list
if kwargs.get("expand"):
kwargs["expand"].extend(expand)
else:
kwargs["expand"] = expand
# Keep only unique elements
# Sort it to ensure the items are returned in the same order
kwargs["expand"] = sorted(list(set(kwargs["expand"])))
return kwargs
@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. \
Defaults 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
"""
# Update kwargs with `expand` param
kwargs = cls.get_expand_params(api_key, **kwargs)
return cls.stripe_class.list(
api_key=api_key,
stripe_version=djstripe_settings.STRIPE_API_VERSION,
**kwargs,
).auto_paging_iter()
class StripeModel(StripeBaseModel):
# This must be defined in descendants of this model/mixin
# e.g. Event, Charge, Customer, etc.
expand_fields: List[str] = []
stripe_dashboard_item_name = ""
objects = models.Manager()
stripe_objects = StripeModelManager()
djstripe_id = models.BigAutoField(
verbose_name="ID", serialize=False, primary_key=True
)
id = StripeIdField(unique=True)
djstripe_owner_account: Optional[StripeForeignKey] = StripeForeignKey(
"djstripe.Account",
on_delete=models.CASCADE,
to_field="id",
null=True,
blank=True,
help_text="The Stripe Account this object belongs to.",
)
livemode = models.BooleanField(
null=True,
default=None,
blank=True,
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."
),
)
created = StripeDateTimeField(
null=True,
blank=True,
help_text="The datetime this object was created in stripe.",
)
metadata = JSONField(
null=True,
blank=True,
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 = models.TextField(
null=True, blank=True, help_text="A description of this object."
)
class Meta(StripeBaseModel.Meta):
abstract = True
get_latest_by = "created"
def _get_base_stripe_dashboard_url(self):
owner_path_prefix = (
(self.djstripe_owner_account.id + "/")
if self.djstripe_owner_account
else ""
)
suffix = "test/" if not self.livemode else ""
return f"https://dashboard.stripe.com/{owner_path_prefix}{suffix}"
def get_stripe_dashboard_url(self) -> str:
"""Get the stripe dashboard url for this object."""
if not self.stripe_dashboard_item_name or not self.id:
return ""
else:
base_url = self._get_base_stripe_dashboard_url()
item = self.stripe_dashboard_item_name
return f"{base_url}{item}/{self.id}"
@property
def default_api_key(self) -> str:
# If the class is abstract (StripeModel), fall back to default key.
if not self._meta.abstract:
if self.djstripe_owner_account:
return self.djstripe_owner_account.get_default_api_key(self.livemode)
return djstripe_settings.get_default_api_key(self.livemode)
def _get_stripe_account_id(self, api_key=None) -> Optional[str]:
"""
Call the stripe API's retrieve operation for this model.
:param api_key: The api key to use for this request. \
Defaults to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
"""
from djstripe.models import Account
api_key = api_key or self.default_api_key
try:
djstripe_owner_account = self.djstripe_owner_account
if djstripe_owner_account is not None:
return djstripe_owner_account.id
except (AttributeError, KeyError, ValueError):
pass
# Get reverse foreign key relations to Account in case we need to
# retrieve ourselves using that Account ID.
reverse_account_relations = (
field
for field in self._meta.get_fields(include_parents=True)
if field.is_relation
and field.one_to_many
and field.related_model is Account
)
# Handle case where we have a reverse relation to Account and should pass
# that account ID to the retrieve call.
for field in reverse_account_relations:
# Grab the related object, using the first one we find.
reverse_lookup_attr = field.get_accessor_name()
try:
account = getattr(self, reverse_lookup_attr).first()
except ValueError:
if isinstance(self, Account):
# return the id if self is the Account model itself.
return self.id
else:
if account is not None:
return account.id
return None
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 djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
"""
api_key = api_key or self.default_api_key
# Prefer passed in stripe_account if set.
if not stripe_account:
stripe_account = self._get_stripe_account_id(api_key)
return self.stripe_class.retrieve(
id=self.id,
api_key=api_key,
stripe_version=djstripe_settings.STRIPE_API_VERSION,
expand=self.expand_fields,
stripe_account=stripe_account,
)
@classmethod
def _api_create(cls, api_key=None, **kwargs):
"""
Call the stripe API's create operation for this model.
:param api_key: The api key to use for this request. \
Defaults to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
"""
livemode = kwargs.pop("livemode", djstripe_settings.STRIPE_LIVE_MODE)
api_key = api_key or djstripe_settings.get_default_api_key(livemode=livemode)
return cls.stripe_class.create(
api_key=api_key,
stripe_version=djstripe_settings.STRIPE_API_VERSION,
**kwargs,
)
def _api_delete(self, api_key=None, stripe_account=None, **kwargs):
"""
Call the stripe API's delete operation for this model
:param api_key: The api key to use for this request. \
Defaults to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
"""
api_key = api_key or self.default_api_key
# Prefer passed in stripe_account if set.
if not stripe_account:
stripe_account = self._get_stripe_account_id(api_key)
return self.stripe_class.delete(
self.id,
api_key=api_key,
stripe_account=stripe_account,
stripe_version=djstripe_settings.STRIPE_API_VERSION,
**kwargs,
)
def _api_update(self, api_key=None, stripe_account=None, **kwargs):
"""
Call the stripe API's modify operation for this model
:param api_key: The api key to use for this request.
Defaults to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
"""
api_key = api_key or self.default_api_key
# Prefer passed in stripe_account if set.
if not stripe_account:
stripe_account = self._get_stripe_account_id(api_key)
return self.stripe_class.modify(
self.id,
api_key=api_key,
stripe_account=stripe_account,
stripe_version=djstripe_settings.STRIPE_API_VERSION,
**kwargs,
)
@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 StripeModel from stripe data.
"""
return data
@classmethod
def _find_owner_account(cls, data, api_key=None, livemode=None):
"""
Fetches the Stripe Account (djstripe_owner_account model field)
linked to the class, cls.
Tries to retreive using the Stripe_account if given.
Otherwise uses the api_key.
"""
from .account import Account
if livemode is None:
livemode = djstripe_settings.STRIPE_LIVE_MODE
api_key = api_key or djstripe_settings.get_default_api_key(livemode)
# try to fetch by stripe_account. Also takes care of Stripe Connected Accounts
if data:
# case of Webhook Event Trigger
if data.get("object") == "event":
# if account key exists and has a not null value
stripe_account_id = get_id_from_stripe_data(data.get("account"))
if stripe_account_id:
return Account._get_or_retrieve(
id=stripe_account_id, api_key=api_key
)
else:
stripe_account = getattr(data, "stripe_account", None)
stripe_account_id = get_id_from_stripe_data(stripe_account)
if stripe_account_id:
return Account._get_or_retrieve(
id=stripe_account_id, api_key=api_key
)
# try to fetch by the given api_key.
return Account.get_or_retrieve_for_api_key(api_key)
@classmethod
def _stripe_object_to_record(
cls,
data: dict,
current_ids=None,
pending_relations: list = None,
stripe_account: str = None,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
) -> Dict:
"""
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
:param current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
:param pending_relations: list of tuples of relations to be attached post-save
:param stripe_account: The optional connected account \
for which this request is being made.
:return: All the members from the input, translated, mutated, etc
"""
from .webhooks import WebhookEndpoint
manipulated_data = cls._manipulate_stripe_object_hook(data)
if not cls.is_valid_object(manipulated_data):
raise ValueError(
"Trying to fit a %r into %r. Aborting."
% (manipulated_data.get("object", ""), cls.__name__)
)
# By default we put the raw stripe data in the stripe_data json field
result = {"stripe_data": data}
if current_ids is None:
current_ids = set()
# Iterate over all the fields that we know are related to Stripe,
# let each field work its own magic
ignore_fields = [
"date_purged",
"subscriber",
"stripe_data",
] # XXX: Customer hack
# get all forward and reverse relations for given cls
for field in cls._meta.get_fields():
if field.name.startswith("djstripe_") or field.name in ignore_fields:
continue
# todo add support reverse ManyToManyField sync
if isinstance(
field, (models.ManyToManyRel, models.ManyToOneRel)
) and not isinstance(field, models.OneToOneRel):
# We don't currently support syncing from
# reverse side of Many relationship
continue
# todo for ManyToManyField one would also need to handle the case of an intermediate model being used
# todo add support ManyToManyField sync
if field.many_to_many:
# We don't currently support syncing ManyToManyField
continue
# will work for Forward FK and OneToOneField relations and reverse OneToOneField relations
if isinstance(field, (models.ForeignKey, models.OneToOneRel)):
field_data, skip, is_nulled = cls._stripe_object_field_to_foreign_key(
field=field,
manipulated_data=manipulated_data,
current_ids=current_ids,
pending_relations=pending_relations,
stripe_account=stripe_account,
api_key=api_key,
)
if skip and not is_nulled:
continue
else:
if hasattr(field, "stripe_to_db"):
field_data = field.stripe_to_db(manipulated_data)
else:
field_data = manipulated_data.get(field.name)
if (
isinstance(field, (models.CharField, models.TextField))
and field_data is None
):
# do not add empty secret field for WebhookEndpoint model
# as stripe does not return the secret except for the CREATE call
if cls is WebhookEndpoint and field.name == "secret":
continue
else:
# TODO - this applies to StripeEnumField as well, since it
# sub-classes CharField, is that intentional?
field_data = ""
result[field.name] = field_data
# For all objects other than the account object itself, get the API key
# attached to the request, and get the matching Account for that key.
owner_account = cls._find_owner_account(data, api_key=api_key)
if owner_account:
result["djstripe_owner_account"] = owner_account
return result
@classmethod
def _stripe_object_field_to_foreign_key(
cls,
field,
manipulated_data,
current_ids=None,
pending_relations=None,
stripe_account=None,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
):
"""
This converts a stripe API field to the dj stripe object it references,
so that foreign keys can be connected up automatically.
:param field:
:type field: models.ForeignKey
:param manipulated_data:
:type manipulated_data: dict
:param current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
:param pending_relations: list of tuples of relations to be attached post-save
:type pending_relations: list
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
:return:
"""
from djstripe.models import DjstripePaymentMethod
field_data = None
field_name = field.name
refetch = False
skip = False
# a flag to indicate if the given field is null upstream on Stripe
is_nulled = False
if current_ids is None:
current_ids = set()
if issubclass(field.related_model, (StripeModel, DjstripePaymentMethod)):
if field_name in manipulated_data:
raw_field_data = manipulated_data.get(field_name)
# field's value is None. Skip syncing but set as None.
# Otherwise nulled FKs sync gets skipped.
if not raw_field_data:
is_nulled = True
skip = True
else:
# field does not exist in manipulated_data dict. Skip Syncing
skip = True
raw_field_data = None
id_ = get_id_from_stripe_data(raw_field_data)
if id_ == raw_field_data:
# A field like {"subscription": "sub_6lsC8pt7IcFpjA", ...}
refetch = True
else:
# A field like {"subscription": {"id": sub_6lsC8pt7IcFpjA", ...}}
pass
if id_ in current_ids:
# this object is currently being fetched, don't try to fetch again,
# to avoid recursion instead, record the relation that should be
# created once "object_id" object exists
if pending_relations is not None:
object_id = manipulated_data["id"]
pending_relations.append((object_id, field, id_))
skip = True
# sync only if field exists and is not null
if not skip and not is_nulled:
# add the id of the current object to the list
# of ids being processed.
# This will avoid infinite recursive syncs in case a relatedmodel
# requests the same object
current_ids.add(id_)
try:
(
field_data,
_,
) = field.related_model._get_or_create_from_stripe_object(
manipulated_data,
field_name,
refetch=refetch,
current_ids=current_ids,
pending_relations=pending_relations,
stripe_account=stripe_account,
api_key=api_key,
)
except ImpossibleAPIRequest:
# Found to happen in the following situation:
# Customer has a `default_source` set to a `card_` object,
# and neither the Customer nor the Card are present in db.
# This skip is a hack, but it will prevent a crash.
skip = True
# Remove the id of the current object from the list
# after it has been created or retrieved
current_ids.remove(id_)
else:
# eg PaymentMethod, handled in hooks
skip = True
return field_data, skip, is_nulled
@classmethod
def is_valid_object(cls, data):
"""
Returns whether the data is a valid object for the class
"""
# .OBJECT_NAME will not exist on the base type itself
object_name: str = getattr(cls.stripe_class, "OBJECT_NAME", "")
if not object_name:
return False
return data and data.get("object") == object_name
def _attach_objects_hook(
self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None
):
"""
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
:param current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
"""
pass
def _attach_objects_post_save_hook(
self,
cls,
data,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
pending_relations=None,
):
"""
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
"""
unprocessed_pending_relations = []
if pending_relations is not None:
for post_save_relation in pending_relations:
object_id, field, id_ = post_save_relation
if self.id == id_:
# the target instance now exists
target = field.model.objects.get(id=object_id)
setattr(target, field.name, self)
if isinstance(field, models.OneToOneRel):
# this is a reverse relationship, so the relation exists on self
self.save()
else:
# this is a forward relation on the target,
# so we need to save it
target.save()
# reload so that indirect relations back to this object
# eg self.charge.invoice = self are set
# TODO - reverse the field reference here to avoid hitting the DB?
self.refresh_from_db()
else:
unprocessed_pending_relations.append(post_save_relation)
if len(pending_relations) != len(unprocessed_pending_relations):
# replace in place so passed in list is updated in calling method
pending_relations[:] = unprocessed_pending_relations
@classmethod
def _create_from_stripe_object(
cls,
data,
current_ids=None,
pending_relations=None,
save=True,
stripe_account=None,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
):
"""
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 current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
:param pending_relations: list of tuples of relations to be attached post-save
:type pending_relations: list
:param save: If True, the object is saved after instantiation.
:type save: bool
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
:returns: The instantiated object.
"""
stripe_data = cls._stripe_object_to_record(
data,
current_ids=current_ids,
pending_relations=pending_relations,
stripe_account=stripe_account,
api_key=api_key,
)
try:
id_ = get_id_from_stripe_data(stripe_data)
if id_ is not None:
instance = cls.stripe_objects.get(id=id_)
else:
# Raise error on purpose to resume the _create_from_stripe_object flow
raise cls.DoesNotExist
except cls.DoesNotExist:
# try to create iff instance doesn't already exist in the DB
# TODO dictionary unpacking will not work if cls has any ManyToManyField
instance = cls(**stripe_data)
instance._attach_objects_hook(
cls, data, api_key=api_key, current_ids=current_ids
)
if save:
instance.save()
instance._attach_objects_post_save_hook(
cls, data, api_key=api_key, pending_relations=pending_relations
)
return instance
@classmethod
def _get_or_create_from_stripe_object(
cls,
data,
field_name="id",
refetch=True,
current_ids=None,
pending_relations=None,
save=True,
stripe_account=None,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
):
"""
:param data:
:param field_name:
:param refetch:
:param current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
:param pending_relations: list of tuples of relations to be attached post-save
:type pending_relations: list
:param save:
:param stripe_account: The optional connected account \
for which this request is being made.
:type stripe_account: string
:return:
:rtype: cls, bool
"""
field = data.get(field_name)
is_nested_data = field_name != "id"
should_expand = False
if not stripe_account and isinstance(data, StripeObject):
stripe_account = data.stripe_account
if pending_relations is None:
pending_relations = []
id_ = get_id_from_stripe_data(field)
if not field:
# An empty field - We need to return nothing here because there is
# no way of knowing what needs to be fetched!
raise RuntimeError(
f"dj-stripe encountered an empty field {cls.__name__}.{field_name} ="
f" {field}"
)
elif id_ == field:
# A field like {"subscription": "sub_6lsC8pt7IcFpjA", ...}
# We'll have to expand if the field is not "id" (= is nested)
should_expand = is_nested_data
else:
# A field like {"subscription": {"id": sub_6lsC8pt7IcFpjA", ...}}
data = field
try:
return cls.stripe_objects.get(id=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(id=id_)
try:
data = cls_instance.api_retrieve(
stripe_account=stripe_account, api_key=api_key
)
except InvalidRequestError as e:
if "a similar object exists in" in str(e):
# HACK around a Stripe bug.
# When a File is retrieved from the Account object,
# a mismatch between live and test mode is possible depending
# on whether the file (usually the logo) was uploaded in live
# or test. Reported to Stripe in August 2020.
# Context: https://github.com/dj-stripe/dj-stripe/issues/830
pass
elif "No such PaymentMethod:" in str(e):
# payment methods (card_… etc) can be irretrievably deleted,
# but still present during sync. For example, if a refund is
# issued on a charge whose payment method has been deleted.
return None, False
elif "Invalid subscription_item id" in str(e):
# subscription items can be irretrievably deleted but still
# be present during sync. For example, if a line item of type subscription is
# removed from the subscription, the invoice generated for that billing period
# will still contain the deleted subscription_item.
return None, False
else:
raise
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!
if should_expand:
raise ValueError(f"No data to create {cls.__name__} from {field_name}")
try:
# We wrap the `_create_from_stripe_object` in a transaction to
# avoid TransactionManagementError on subsequent queries in case
# of the IntegrityError catch below. See PR #903
with transaction.atomic():
return (
cls._create_from_stripe_object(
data,
current_ids=current_ids,
pending_relations=pending_relations,
save=save,
stripe_account=stripe_account,
api_key=api_key,
),
True,
)
except IntegrityError:
# Handle the race condition that something else created the object
# after the `get` and before `_create_from_stripe_object`.
# This is common during webhook handling, since Stripe sends
# multiple webhook events simultaneously,
# each of which will cause recursive syncs. See issue #429
return cls.stripe_objects.get(id=id_), False
@classmethod
def _stripe_object_to_customer(
cls,
target_cls,
data,
api_key=djstripe_settings.STRIPE_SECRET_KEY,
current_ids=None,
):
"""
Search the given manager for the Customer matching this object's
``customer`` field.
:param target_cls: The target class
:type target_cls: Customer
:param data: stripe object
:type data: dict
:param current_ids: stripe ids of objects that are currently being processed
:type current_ids: set
"""
if "customer" in data and data["customer"]:
return target_cls._get_or_create_from_stripe_object(
data, "customer", current_ids=current_ids, api_key=api_key
)[0]
@classmethod
def _stripe_object_to_default_tax_rates(
cls, target_cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY
):
"""
Retrieves TaxRates for a Subscription or Invoice
:param target_cls:
:param data:
:param instance:
:type instance: Union[djstripe.models.Invoice, djstripe.models.Subscription]
:return:
"""
tax_rates = []
for tax_rate_data in data.get("default_tax_rates", []):
tax_rate, _ = target_cls._get_or_create_from_stripe_object(
tax_rate_data, refetch=False, api_key=api_key
)
tax_rates.append(tax_rate)
return tax_rates
@classmethod
def _stripe_object_to_tax_rates(
cls, target_cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY
):
"""
Retrieves TaxRates for a SubscriptionItem or InvoiceItem
:param target_cls:
:param data:
:return:
"""
tax_rates = []
for tax_rate_data in data.get("tax_rates", []):
tax_rate, _ = target_cls._get_or_create_from_stripe_object(
tax_rate_data, refetch=False, api_key=api_key
)
tax_rates.append(tax_rate)
return tax_rates
@classmethod
def _stripe_object_set_total_tax_amounts(
cls, target_cls, data, instance, api_key=djstripe_settings.STRIPE_SECRET_KEY
):
"""
Set total tax amounts on Invoice instance
:param target_cls:
:param data:
:param instance:
:type instance: djstripe.models.Invoice
:return:
"""
from .billing import TaxRate
pks = []
for tax_amount_data in data.get("total_tax_amounts", []):
tax_rate_data = tax_amount_data["tax_rate"]
if isinstance(tax_rate_data, str):
tax_rate_data = {"tax_rate": tax_rate_data}
tax_rate, _ = TaxRate._get_or_create_from_stripe_object(
tax_rate_data,
field_name="tax_rate",
refetch=True,
api_key=api_key,
)
tax_amount, _ = target_cls.objects.update_or_create(
invoice=instance,
tax_rate=tax_rate,
defaults={
"amount": tax_amount_data["amount"],
"inclusive": tax_amount_data["inclusive"],
},
)
pks.append(tax_amount.pk)
instance.total_tax_amounts.exclude(pk__in=pks).delete()
@classmethod
def _stripe_object_to_line_items(
cls, target_cls, data, invoice, api_key=djstripe_settings.STRIPE_SECRET_KEY
):
"""
Retrieves LineItems for an invoice.
If the line 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 line items are also not saved.
:param target_cls: The target class to instantiate per line item.
:type target_cls: Type[djstripe.models.LineItem]
:param data: The data dictionary received from the Stripe API.
:type data: dict
:param invoice: The invoice object that should hold the line items.
:type invoice: ``djstripe.models.Invoice``
"""
lines = data.get("lines")
if not lines:
return []
lineitems = []
for line in lines.auto_paging_iter():
if invoice.id:
save = True
line.setdefault("invoice", invoice.id)
else:
# Don't save invoice items for ephemeral invoices
save = False
line.setdefault("customer", invoice.customer.id)
line.setdefault("date", int(dateformat.format(invoice.created, "U")))
item, _ = target_cls._get_or_create_from_stripe_object(
line, refetch=False, save=save, api_key=api_key
)
lineitems.append(item)
return lineitems
@classmethod
def _stripe_object_to_subscription_items(
cls, target_cls, data, subscription, api_key=djstripe_settings.STRIPE_SECRET_KEY
):
"""
Retrieves SubscriptionItems for a subscription.
If the subscription item doesn't exist already then it is created.
:param target_cls: The target class to instantiate per invoice item.
:type target_cls: Type[djstripe.models.SubscriptionItem]
:param data: The data dictionary received from the Stripe API.
:type data: dict
:param subscription: The subscription object that should hold the items.
:type subscription: djstripe.models.Subscription
"""
items = data.get("items")
if not items:
subscription.items.delete()
return []