Repository: dj-stripe/dj-stripe

Issue:
PaymentMethod detachment doesn't work because it doesn't unset customer
**Describe the bug**
I'm using 4a828a48092a3904094917776ce725ec9aa3fce5 (after the #914 merge) and trying to set up an SCA complaint flow.
As I said in #941, the events don't sync this model. Though even if I sync it manually (using sync_from_stripe_data), it doesn't work correctly for the detached payment_method case. The customer isn't actually detached (the content of the stripe event is `customer=null`)

**To Reproduce**
Steps to reproduce the behavior:
1. Detach a payment method from the stripe api
2. Try to sync the payment method object: `payment_method.sync_from_stripe_data(payment_method.api_retrieve())`
3. Observe that the customer association isn't removed even though stripe returns `customer=null`.

**Expected behavior**
The payment method should get detached from the customer.

Example event:
```
{
  "object": {
    "id": "pm_1F8uMTHa6wE0PhFmIrAlCMuB",
    "object": "payment_method",
    "billing_details": {
      "address": {
        "city": null,
        "country": "GB",
        "line1": null,
        "line2": null,
        "postal_code": "W1",
        "state": null
      },
      "email": "me@etesync.com",
      "name": "Jeo",
      "phone": null
    },
    "card": {
      "brand": "visa",
      "checks": {
        "address_line1_check": null,
        "address_postal_code_check": "pass",
        "cvc_check": "pass"
      },
      "country": "US",
      "exp_month": 2,
      "exp_year": 2022,
      "fingerprint": "No6qZ6uMjc9xCthT",
      "funding": "credit",
      "generated_from": null,
      "last4": "4242",
      "three_d_secure_usage": {
        "supported": true
      },
      "wallet": null
    },
    "created": 1566157701,
    "customer": null,
    "livemode": false,
    "metadata": {
    },
    "type": "card"
  },
  "previous_attributes": {
    "customer": "cus_FXgo0IlbgH2cQS"
  }
}```

**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**
Add any other context about the problem here.



Discussion:

Resolving patch:
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
@@ -527,6 +527,13 @@ class PaymentMethod(StripeModel):
 
     stripe_class = stripe.PaymentMethod
 
+    def _attach_objects_hook(self, cls, data):
+        customer = cls._stripe_object_to_customer(target_cls=Customer, data=data)
+        if customer:
+            self.customer = customer
+        else:
+            self.customer = None
+
     @classmethod
     def attach(
         cls, payment_method, customer, api_key=djstripe_settings.STRIPE_SECRET_KEY


Test patch:
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
@@ -78,3 +78,24 @@ def test_attach_synced(self, attach_mock):
                 "djstripe.Customer.default_payment_method",
             },
         )
+
+    def test_sync_null_customer(self):
+        payment_method = PaymentMethod.sync_from_stripe_data(
+            deepcopy(FAKE_PAYMENT_METHOD_I)
+        )
+
+        self.assertIsNotNone(payment_method.customer)
+
+        # simulate remote detach
+        fake_payment_method_no_customer = deepcopy(FAKE_PAYMENT_METHOD_I)
+        fake_payment_method_no_customer["customer"] = None
+
+        payment_method = PaymentMethod.sync_from_stripe_data(
+            fake_payment_method_no_customer
+        )
+
+        self.assertIsNone(payment_method.customer)
+
+        self.assert_fks(
+            payment_method, expected_blank_fks={"djstripe.PaymentMethod.customer"}
+        )