Repository: dj-stripe/dj-stripe

Issue:
Customer "can_charge" doesn't work with payment_method
**Describe the bug**
This is something I already flagged in #914, but I guess it slipped through.
The Customer model's "can_charge" only checks if the customer has a valid source, but with payment_methods you now need to check if there's maybe a valid payment_method too.

**To Reproduce**
1. Create a customer
2. Add a payment_method (assuming #941 is fixed)
3. Call customer.can_charge()
4. It returns false

**Expected behavior**
It should return true if there are payment_methods attached to the customer.

**Environment**
- dj-stripe version: master at 4a828a48092a3904094917776ce725ec9aa3fce5
- Your Stripe account's default API version: [e.g. 2019-02-19 - shown as "default" on https://dashboard.stripe.com/developers]
- Database: Postgres
- Python version: 3.7.4
- Django version: 2.2.4

**Can you reproduce the issue with the latest version of master?**

Yes

**Additional context**
Customer "can_charge" doesn't work with payment_method
**Describe the bug**
This is something I already flagged in #914, but I guess it slipped through.
The Customer model's "can_charge" only checks if the customer has a valid source, but with payment_methods you now need to check if there's maybe a valid payment_method too.

**To Reproduce**
1. Create a customer
2. Add a payment_method (assuming #941 is fixed)
3. Call customer.can_charge()
4. It returns false

**Expected behavior**
It should return true if there are payment_methods attached to the customer.

**Environment**
- dj-stripe version: master at 4a828a48092a3904094917776ce725ec9aa3fce5
- Your Stripe account's default API version: [e.g. 2019-02-19 - shown as "default" on https://dashboard.stripe.com/developers]
- Database: Postgres
- Python version: 3.7.4
- Django version: 2.2.4

**Can you reproduce the issue with the latest version of master?**

Yes

**Additional context**


Discussion:

Resolving patch:
diff --git a/djstripe/migrations/0015_customer_default_payment_method.py b/djstripe/migrations/0015_customer_default_payment_method.py
new file mode 100644
--- /dev/null
+++ b/djstripe/migrations/0015_customer_default_payment_method.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.4 on 2019-09-08 01:38
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("djstripe", "0014_auto_20190906_2013")]
+
+    operations = [
+        migrations.AddField(
+            model_name="customer",
+            name="default_payment_method",
+            field=models.ForeignKey(
+                blank=True,
+                help_text="default payment method used for subscriptions and invoices for the customer.",
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="+",
+                to="djstripe.PaymentMethod",
+            ),
+        )
+    ]
diff --git a/djstripe/models/core.py b/djstripe/models/core.py
--- a/djstripe/models/core.py
+++ b/djstripe/models/core.py
@@ -437,6 +437,17 @@ class Customer(StripeModel):
     invoice_settings = JSONField(
         null=True, blank=True, help_text="The customer's default invoice settings."
     )
+    # default_payment_method is actually nested inside invoice_settings
+    # this field is a convenience to provide the foreign key
+    default_payment_method = models.ForeignKey(
+        "PaymentMethod",
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        help_text="default payment method used for subscriptions and invoices "
+        "for the customer.",
+    )
     name = models.TextField(
         max_length=5000,
         default="",
@@ -495,6 +506,11 @@ def _manipulate_stripe_object_hook(cls, data):
             data["coupon_start"] = discount["start"]
             data["coupon_end"] = discount["end"]
 
+        # Populate the object id for our default_payment_method field (or set it None)
+        data["default_payment_method"] = data.get("invoice_settings", {}).get(
+            "default_payment_method"
+        )
+
         return data
 
     @classmethod
@@ -865,20 +881,35 @@ def add_card(self, source, set_default=True):
 
         return new_payment_method.resolve()
 
-    # TODO - support setting default payment method
-    #  (as per set_default param to add_card), see
-    #  see https://stripe.com/docs/api/payment_methods/attach
-    def add_payment_method(self, payment_method_id):
+    def add_payment_method(self, payment_method, set_default=True):
         """
         Adds an already existing payment method to this customer's account
 
-        :param payment_method_id: ID of the PaymentMethod to be attached to the customer
+        :param payment_method: PaymentMethod to be attached to the customer
+        :type payment_method: str, PaymentMethod
+        :param set_default: If true, this will be set as the default_payment_method
+        :type: bool
         :return:
         """
         from .payment_methods import PaymentMethod
 
         stripe_customer = self.api_retrieve()
-        PaymentMethod.attach(payment_method_id, stripe_customer)
+        payment_method = PaymentMethod.attach(payment_method, stripe_customer)
+
+        if set_default:
+            stripe_customer["invoice_settings"][
+                "default_payment_method"
+            ] = payment_method.id
+            stripe_customer.save()
+
+            # Refresh self from the stripe customer, this should have two effects:
+            # 1) sets self.default_payment_method (we rely on logic in
+            # Customer._manipulate_stripe_object_hook to do this)
+            # 2) updates self.invoice_settings.default_payment_methods
+            self.sync_from_stripe_data(stripe_customer)
+            self.refresh_from_db()
+
+        return payment_method
 
     def purge(self):
         try:
@@ -1028,7 +1059,9 @@ def subscription(self):
     def can_charge(self):
         """Determines if this customer is able to be charged."""
 
-        return self.has_valid_source() and self.date_purged is None
+        return (
+            self.has_valid_source() or self.default_payment_method is not None
+        ) and self.date_purged is None
 
     def send_invoice(self):
         """
diff --git a/djstripe/models/payment_methods.py b/djstripe/models/payment_methods.py
--- a/djstripe/models/payment_methods.py
+++ b/djstripe/models/payment_methods.py
@@ -529,12 +529,32 @@ class PaymentMethod(StripeModel):
 
     @classmethod
     def attach(
-        cls,
-        payment_method_id,
-        stripe_customer,
-        api_key=djstripe_settings.STRIPE_SECRET_KEY,
+        cls, payment_method, customer, api_key=djstripe_settings.STRIPE_SECRET_KEY
     ):
+        """
+        Attach a payment method to a customer
+        :param payment_method:
+        :type payment_method: str, PaymentMethod
+        :param customer:
+        :type customer: str, Customer
+        :param api_key:
+        :return:
+        """
+
+        if isinstance(payment_method, StripeModel):
+            payment_method = payment_method.id
+
+        if isinstance(customer, StripeModel):
+            customer = customer.id
+
+        extra_kwargs = {}
+        if not isinstance(payment_method, stripe.PaymentMethod):
+            # send api_key if we're not passing in a Stripe object
+            # avoids "Received unknown parameter: api_key" since api uses the
+            # key cached in the Stripe object
+            extra_kwargs = {"api_key": api_key}
+
         stripe_payment_method = stripe.PaymentMethod.attach(
-            payment_method_id, customer=stripe_customer["id"], api_key=api_key
+            payment_method, customer=customer, **extra_kwargs
         )
         return cls.sync_from_stripe_data(stripe_payment_method)


Test patch:
diff --git a/tests/test_bank_account.py b/tests/test_bank_account.py
--- a/tests/test_bank_account.py
+++ b/tests/test_bank_account.py
@@ -46,6 +46,7 @@ def test_create_bank_account_finds_customer(self):
             bank_account,
             expected_blank_fks={
                 "djstripe.BankAccount.account",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.default_source",
                 "djstripe.Customer.coupon",
             },
diff --git a/tests/test_card.py b/tests/test_card.py
--- a/tests/test_card.py
+++ b/tests/test_card.py
@@ -50,6 +50,7 @@ def test_create_card_finds_customer(self):
             expected_blank_fks={
                 "djstripe.BankAccount.account",
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.default_source",
             },
         )
@@ -74,6 +75,7 @@ def test_str(self):
             card,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.default_source",
             },
         )
diff --git a/tests/test_charge.py b/tests/test_charge.py
--- a/tests/test_charge.py
+++ b/tests/test_charge.py
@@ -45,6 +45,7 @@ def setUp(self):
             "djstripe.Charge.dispute",
             "djstripe.Charge.transfer",
             "djstripe.Customer.coupon",
+            "djstripe.Customer.default_payment_method",
             "djstripe.PaymentIntent.on_behalf_of",
             "djstripe.PaymentIntent.payment_method",
             "djstripe.Subscription.pending_setup_intent",
diff --git a/tests/test_customer.py b/tests/test_customer.py
--- a/tests/test_customer.py
+++ b/tests/test_customer.py
@@ -20,6 +20,7 @@
     DjstripePaymentMethod,
     IdempotencyKey,
     Invoice,
+    PaymentMethod,
     Plan,
     Subscription,
 )
@@ -166,6 +167,7 @@ def test_customer_sync_has_subscriber_metadata_disabled(self):
             customer,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.subscriber",
             },
         )
@@ -183,6 +185,7 @@ def test_customer_sync_has_bad_subscriber_metadata(self):
             customer,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.subscriber",
             },
         )
@@ -211,6 +214,7 @@ def test_customer_create_metadata_disabled(self, customer_mock):
             customer,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.default_source",
             },
         )
@@ -256,7 +260,13 @@ def test_customer_sync_bank_account_source(self, bank_account_retrieve_mock):
             customer.default_source.id, fake_customer["default_source"]["id"]
         )
 
-        self.assert_fks(customer, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            customer,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
     @patch("stripe.Customer.create", autospec=True)
     def test_customer_sync_no_sources(self, customer_mock):
@@ -283,6 +293,7 @@ def test_customer_sync_no_sources(self, customer_mock):
             customer,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.default_source",
             },
         )
@@ -303,6 +314,7 @@ def test_customer_sync_default_source_string(self):
             customer,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.subscriber",
             },
         )
@@ -448,7 +460,53 @@ def test_add_card_set_default_false_with_single_card_still_becomes_default(
         "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True
     )
     @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
-    def test_add_payment_method(self, customer_retrieve_mock, attach_mock):
+    def test_add_payment_method_obj(self, attach_mock, customer_retrieve_mock):
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).count(),
+            0,
+        )
+
+        payment_method = PaymentMethod.sync_from_stripe_data(FAKE_PAYMENT_METHOD_I)
+        payment_method = self.customer.add_payment_method(payment_method)
+
+        self.assertEqual(payment_method.customer.id, self.customer.id)
+
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).count(),
+            1,
+        )
+
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).first(),
+            self.customer.default_payment_method,
+        )
+
+        self.assertEqual(
+            self.customer.default_payment_method.id,
+            self.customer.invoice_settings["default_payment_method"],
+        )
+
+        self.assert_fks(self.customer, expected_blank_fks={"djstripe.Customer.coupon"})
+
+    @patch("stripe.Customer.retrieve", autospec=True)
+    @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
+    def test_add_payment_method_set_default_true(
+        self, attach_mock, customer_retrieve_mock
+    ):
+        # clear default source so we can check can_charge()
+        fake_customer = deepcopy(FAKE_CUSTOMER)
+        fake_customer["default_source"] = None
+        customer_retrieve_mock.return_value = fake_customer
+
+        self.customer.default_source = None
+        self.customer.save()
+
         self.assertEqual(
             self.customer.payment_methods.filter(
                 id=FAKE_PAYMENT_METHOD_I["id"]
@@ -456,7 +514,9 @@ def test_add_payment_method(self, customer_retrieve_mock, attach_mock):
             0,
         )
 
-        self.customer.add_payment_method(FAKE_PAYMENT_METHOD_I["id"])
+        payment_method = self.customer.add_payment_method(FAKE_PAYMENT_METHOD_I["id"])
+
+        self.assertEqual(payment_method.customer.id, self.customer.id)
 
         self.assertEqual(
             self.customer.payment_methods.filter(
@@ -465,6 +525,79 @@ def test_add_payment_method(self, customer_retrieve_mock, attach_mock):
             1,
         )
 
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).first(),
+            self.customer.default_payment_method,
+        )
+
+        self.assertEqual(
+            self.customer.default_payment_method.id,
+            self.customer.invoice_settings["default_payment_method"],
+        )
+
+        self.assertTrue(
+            self.customer.can_charge(),
+            "Expect to be able to charge since we've set a default_payment_method",
+        )
+
+        self.assert_fks(
+            self.customer,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_source",
+            },
+        )
+
+    @patch("stripe.Customer.retrieve", autospec=True)
+    @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
+    def test_add_payment_method_set_default_false(
+        self, attach_mock, customer_retrieve_mock
+    ):
+        # clear default source so we can check can_charge()
+        fake_customer = deepcopy(FAKE_CUSTOMER)
+        fake_customer["default_source"] = None
+        customer_retrieve_mock.return_value = fake_customer
+
+        self.customer.default_source = None
+        self.customer.save()
+
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).count(),
+            0,
+        )
+
+        payment_method = self.customer.add_payment_method(
+            FAKE_PAYMENT_METHOD_I["id"], set_default=False
+        )
+
+        self.assertEqual(payment_method.customer.id, self.customer.id)
+
+        self.assertEqual(
+            self.customer.payment_methods.filter(
+                id=FAKE_PAYMENT_METHOD_I["id"]
+            ).count(),
+            1,
+        )
+
+        self.assertFalse(
+            self.customer.can_charge(),
+            "Expect not to be able to charge since we've not set a "
+            "default_payment_method",
+        )
+
+        self.assert_fks(
+            self.customer,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+                "djstripe.Customer.default_source",
+            },
+        )
+
     @patch(
         "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True
     )
@@ -515,7 +648,10 @@ def fake_customer_save(self, *args, **kwargs):
 
         self.customer.refresh_from_db()
 
-        self.assert_fks(self.customer, expected_blank_fks={})
+        self.assert_fks(
+            self.customer,
+            expected_blank_fks={"djstripe.Customer.default_payment_method"},
+        )
 
     @patch(
         "djstripe.models.Account.get_default_account",
@@ -562,6 +698,7 @@ def test_refund_charge(
                 "djstripe.Charge.invoice",
                 "djstripe.Charge.transfer",
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
                 "djstripe.PaymentIntent.payment_method",
@@ -588,6 +725,7 @@ def test_refund_charge(
                 "djstripe.Charge.invoice",
                 "djstripe.Charge.transfer",
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
                 "djstripe.PaymentIntent.payment_method",
@@ -639,6 +777,7 @@ def test_refund_charge_object_returned(
                 "djstripe.Charge.invoice",
                 "djstripe.Charge.transfer",
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
                 "djstripe.PaymentIntent.payment_method",
@@ -659,6 +798,7 @@ def test_refund_charge_object_returned(
                 "djstripe.Charge.invoice",
                 "djstripe.Charge.transfer",
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
                 "djstripe.PaymentIntent.payment_method",
diff --git a/tests/test_event_handlers.py b/tests/test_event_handlers.py
--- a/tests/test_event_handlers.py
+++ b/tests/test_event_handlers.py
@@ -763,7 +763,13 @@ def test_payment_method_attached(
             id=fake_stripe_event["data"]["object"]["id"]
         )
 
-        self.assert_fks(payment_method, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            payment_method,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
 
 class TestTransferEvents(EventTestCase):
diff --git a/tests/test_invoice.py b/tests/test_invoice.py
--- a/tests/test_invoice.py
+++ b/tests/test_invoice.py
@@ -43,6 +43,7 @@ def setUp(self):
             "djstripe.Charge.dispute",
             "djstripe.Charge.transfer",
             "djstripe.Customer.coupon",
+            "djstripe.Customer.default_payment_method",
             "djstripe.PaymentIntent.on_behalf_of",
             "djstripe.PaymentIntent.payment_method",
             "djstripe.Subscription.pending_setup_intent",
diff --git a/tests/test_invoiceitem.py b/tests/test_invoiceitem.py
--- a/tests/test_invoiceitem.py
+++ b/tests/test_invoiceitem.py
@@ -35,6 +35,7 @@ def setUp(self):
             "djstripe.Charge.payment_intent",
             "djstripe.Charge.transfer",
             "djstripe.Customer.coupon",
+            "djstripe.Customer.default_payment_method",
             "djstripe.Customer.subscriber",
             "djstripe.Invoice.payment_intent",
             "djstripe.Subscription.pending_setup_intent",
diff --git a/tests/test_payment_intent.py b/tests/test_payment_intent.py
--- a/tests/test_payment_intent.py
+++ b/tests/test_payment_intent.py
@@ -23,6 +23,7 @@ def test_sync_from_stripe_data(self, customer_retrieve_mock):
             payment_intent,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.subscriber",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
diff --git a/tests/test_payment_method.py b/tests/test_payment_method.py
--- a/tests/test_payment_method.py
+++ b/tests/test_payment_method.py
@@ -24,18 +24,38 @@ def setUp(self):
         )
         self.customer = FAKE_CUSTOMER.create_for_user(self.user)
 
-    # TODO - this should use autospec=True, but it's failing for some reason
-    #   with unexpected keyword argument "customer"
+    # TODO - these should use autospec=True with stripe.PaymentMethod.attach,
+    #  but it's failing for some reason with:
+    #  TypeError: got an unexpected keyword argument 'customer'
+
     @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
     def test_attach(self, attach_mock):
         payment_method = PaymentMethod.attach(
-            FAKE_PAYMENT_METHOD_I["id"], stripe_customer=FAKE_CUSTOMER
+            FAKE_PAYMENT_METHOD_I["id"], customer=FAKE_CUSTOMER["id"]
         )
 
-        self.assert_fks(payment_method, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            payment_method,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
+
+    @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
+    def test_attach_obj(self, attach_mock):
+        pm = PaymentMethod.sync_from_stripe_data(FAKE_PAYMENT_METHOD_I)
+
+        payment_method = PaymentMethod.attach(pm, customer=self.customer)
+
+        self.assert_fks(
+            payment_method,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
-    # TODO - this should use autospec=True, but it's failing for some reason
-    #   with unexpected keyword argument "customer"
     @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I))
     def test_attach_synced(self, attach_mock):
         fake_payment_method = deepcopy(FAKE_PAYMENT_METHOD_I)
@@ -48,7 +68,13 @@ def test_attach_synced(self, attach_mock):
         )
 
         payment_method = PaymentMethod.attach(
-            payment_method.id, stripe_customer=FAKE_CUSTOMER
+            payment_method.id, customer=FAKE_CUSTOMER["id"]
         )
 
-        self.assert_fks(payment_method, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            payment_method,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
diff --git a/tests/test_session.py b/tests/test_session.py
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -35,6 +35,7 @@ def test_sync_from_stripe_data(
             session,
             expected_blank_fks={
                 "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
                 "djstripe.Customer.subscriber",
                 "djstripe.PaymentIntent.invoice (related name)",
                 "djstripe.PaymentIntent.on_behalf_of",
diff --git a/tests/test_source.py b/tests/test_source.py
--- a/tests/test_source.py
+++ b/tests/test_source.py
@@ -34,14 +34,26 @@ def test_attach_objects_hook_without_customer(self):
         source = Source.sync_from_stripe_data(deepcopy(FAKE_SOURCE_II))
         self.assertEqual(source.customer, None)
 
-        self.assert_fks(source, expected_blank_fks={"djstripe.Source.customer"})
+        self.assert_fks(
+            source,
+            expected_blank_fks={
+                "djstripe.Source.customer",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
     def test_sync_source_finds_customer(self):
         source = Source.sync_from_stripe_data(deepcopy(FAKE_SOURCE))
 
         self.assertEqual(self.customer, source.customer)
 
-        self.assert_fks(source, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            source,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
     def test_str(self):
         fake_source = deepcopy(FAKE_SOURCE)
@@ -49,7 +61,13 @@ def test_str(self):
 
         self.assertEqual("<id={}>".format(fake_source["id"]), str(source))
 
-        self.assert_fks(source, expected_blank_fks={"djstripe.Customer.coupon"})
+        self.assert_fks(
+            source,
+            expected_blank_fks={
+                "djstripe.Customer.coupon",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
 
     @patch("stripe.Source.retrieve", return_value=deepcopy(FAKE_SOURCE), autospec=True)
     def test_detach(self, source_retrieve_mock):
@@ -86,4 +104,10 @@ def mocked_detach(self):
             # for the test
             mock_detach.assert_called()
 
-        self.assert_fks(source, expected_blank_fks={"djstripe.Source.customer"})
+        self.assert_fks(
+            source,
+            expected_blank_fks={
+                "djstripe.Source.customer",
+                "djstripe.Customer.default_payment_method",
+            },
+        )
diff --git a/tests/test_subscription.py b/tests/test_subscription.py
--- a/tests/test_subscription.py
+++ b/tests/test_subscription.py
@@ -39,6 +39,7 @@ def setUp(self):
 
         self.default_expected_blank_fks = {
             "djstripe.Customer.coupon",
+            "djstripe.Customer.default_payment_method",
             "djstripe.Subscription.pending_setup_intent",
         }