dj-stripe / dj-stripe

dj-stripe automatically syncs your Stripe Data to your local database as pre-implemented Django Models allowing you to use the Django ORM, in your code, to work with the data making it easier and faster.
https://dj-stripe.dev
MIT License
1.56k stars 476 forks source link

Extra/Empty Customer Instance Created During Checkout #1445

Closed alecdalelio closed 2 years ago

alecdalelio commented 2 years ago

Describe the bug

DJ-stripe is creating an additional customer object for each of my real Stripe customers upon creation. This extra (blank) customer is the one that is related to my User model, so when I try to access Stripe Customer data via the authenticated User on my site, the data comes back empty. The original Stripe Customer object (with all of the correct data) still lives in my database but is not related to a User.

I don't know if this is a bug or an implementation issue on my end, but if this rings a bell for anyone I'd appreciate your help. I am 99% finished with my integration, but I am unable to ship the Stripe Customer Portal because it is using the blank/extra Stripe Customer and therefore does not display any data as it should.

Software versions

Steps To Reproduce

This is my local configuration - to reproduce the issue your environment should match up with: STRIPE_TEST_SECRET_KEY = reach out to dev team for this STRIPE_TEST_PUBLIC_KEY = reach out to dev team for this DJSTRIPE_WEBHOOK_SECRET = reach out to dev team for this DJSTRIPE_WEBHOOK_VALIDATION = "verify_signature" DJSTRIPE_USE_NATIVE_JSONFIELD = True DJSTRIPE_FOREIGN_KEY_TO_FIELD = "djstripe_id" DJSTRIPE_SUBSCRIBER_MODEL = "myApp.User" DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY = "djstripe_subscriber" DJSTRIPE_WEBHOOK_URL=r"^stripe/\$"

If you have a matching backend configuration and a frontend which allows you to complete the Stripe checkout process, you can reproduce the issue. Simply go through the standard Stripe checkout flow and then check your database to see if two records were created instead of one. One should have the full data that you entered in Checkout (credit card number, subscription, etc.) and the other one should simply have the name and email address.

Better yet, if you have the Stripe Customer Portal integrated on your frontend you may be able to see what I mean when the empty/inactive/extra Stripe Customer ID is presented instead of the one that maps to the request user.

Can you reproduce the issue with the latest version of master?

I believe so

Expected Behavior

I expect DJ-stripe to relate the request User with the corresponding Stripe Customer instance so that I can query and access this data on my frontend and allow users to interact with the Stripe Customer Portal to manage their membership.

Actual Behavior

There should be two database records and two Stripe customers viewable in your Stripe dashboard with the same email address. One of them will also have customer data such as payment method. The other one will be empty beyond email/name. The empty one will be related to an instance of .User.

arnav13081994 commented 2 years ago

@alecdalelio Can you paste you Stripe Checkout Session code? Please note that Stripe will create a new customer object even for the same customer email. Only if you send the customer_id in the Checkout Session will it link that Session to the provided customer and a new customer will not get created.

Also please checkout this PR https://github.com/dj-stripe/dj-stripe/pull/1416 Seems relevant

arnav13081994 commented 2 years ago

Closing this due to no response. Feel free to re-open if you encounter this issue again.

alecdalelio commented 2 years ago

@arnav13081994 - I have a checkout page where users can toggle between two options for checkout: annual ($110/yr) or quarterly ($30/quarter). Below is the handleClick() method which calls my backend server to create a checkout session using the Stripe price_id corresponding to annual membership and redirects to it.

const handleClick = async (event) => {
    const stripe = await stripePromise;
    const stripeSessionData = await axios.post(
      `${API_ENDPOINT}/payments/session/`,
      { price_id: ANNUAL_PRICE_ID }
    );
    const sessionId = stripeSessionData.data["session_id"];
    await stripe.redirectToCheckout({
      sessionId,
    });
  };

Here is the backend endpoint in question: this is what we discussed in Discord which you recommended switching from POST to GET.

class CheckoutSessionApiView(APIView):
    """
    Checkout view for membership payments
    """

    def get(self, request):
        """
        Create Stripe checkout session
        """

        try:
            id = request.user.id

        except AttributeError:
            id = (
                djstripe_settings.get_subscriber_model().objects.create(email=request.user.email).id
            )

        try:
            metadata = {f"{djstripe_settings.SUBSCRIBER_CUSTOMER_KEY}": id}
            session = stripe.checkout.Session.create(
                payment_method_types=["card"],
                line_items=[{"price": request.data["price_id"], "quantity": 1}],
                mode="subscription",
                client_reference_id=request.user.id,
                success_url=FRONTEND_URL + "/profile/" + str(request.user.id),
                cancel_url=FRONTEND_URL,
                customer_email=request.user.email,
                metadata=metadata
            )

            return Response({"session_id": session.id}, status=status.HTTP_200_OK)
        except ValueError:
            return Response(status=status.HTTP_404_NOT_FOUND)

The above Django view worked as expected as a post() besides the fact that it still creates two customers upon completing the checkout session. Since switching to get() as you suggested, I receive the following 500 error on my backend server when I attempt to create/redirect to a checkout session:

line_items=[{"price": request.data["price_id"], "quantity": 1}], KeyError: 'price_id'

arnav13081994 commented 2 years ago

GET will not work for your flow as you are sending data from the frontend to create the session. Although you could just have the price_id as a url kwarg and then you could call the url that triggers the CheckoutSession with that kwarg. But lets keep this as a backup. The way you are sending data to the checkout session shouldn't matter anyway.

Just to make sure I understand your flow, this is what you are doing:

1) The user is on some pricing page (not Stripe Checkout) and has 2 options to select from. 2) They select one and then click on a button or link that triggers the handleClick method. 3) The backend view is hit, which creates the appropriate checkout session and returns the session id. 3) The user is then redirected to the Stripe Checkout Page that uses the session id returned in 3.

Is the flow correct?

Also do not do:

        try:
            id = request.user.id

        except AttributeError:
            id = (
                djstripe_settings.get_subscriber_model().objects.create(email=request.user.email).id
            )

Unless you want to create a new user and you are confident about that. The purpose of the example was different. Which model is your djstripe_settings.get_subscriber_model()?

alecdalelio commented 2 years ago

@arnav13081994 Yes the flow is correct.

Assuming djstripe_settings.get_subscriber_model() corresponds to the DJSTRIPE_SUBSCRIBER_MODEL environment variable, my subscriber model in this case is simply myApp.User.

arnav13081994 commented 2 years ago

Ok and I assume you simply overrode the built-in User model provided by Django out of the box, right?

And how do you handle the returned {"session_id": session.id} from this view?

Change the view to this:

class CheckoutSessionApiView(APIView):
    """
    Checkout view for membership payments
    """

    def post(self, request):
        """
        Create Stripe checkout session
        """
         # could do some error handling but that's up to you and if you expect 
         # request.user.id to return None and throw an error
         id = request.user.id 
         metadata = {f"{djstripe_settings.SUBSCRIBER_CUSTOMER_KEY}": id}

        try:

            session = stripe.checkout.Session.create(
                payment_method_types=["card"],
                line_items=[{"price": request.data["price_id"], "quantity": 1}],
                mode="subscription",
                client_reference_id=request.user.id,
                success_url=FRONTEND_URL + "/profile/" + str(request.user.id),
                cancel_url=FRONTEND_URL,
                customer_email=request.user.email,
                metadata=metadata
            )

            return Response({"session_id": session.id}, status=status.HTTP_200_OK)
        except ValueError:
            return Response(status=status.HTTP_404_NOT_FOUND)
alecdalelio commented 2 years ago

@arnav13081994 Correct - I am using a custom user model. I'll give this a go - thanks!

The session ID returned from the backend is used to redirect the user to the checkout session here (see the full handleClick() method above for more context)

    const stripeSessionData = await axios.post(
      `${API_ENDPOINT}/payments/session/`,
      { price_id: ANNUAL_PRICE_ID }
    );
    const sessionId = stripeSessionData.data["session_id"];
    await stripe.redirectToCheckout({
      sessionId,
    });
alecdalelio commented 2 years ago

@arnav13081994 Just gave it a shot after making the changes you suggested. Nothing changed unfortunately.

Screen Shot 2021-10-31 at 11 43 13 PM

d.

arnav13081994 commented 2 years ago

Up until now, I was under the impression that your local db had duplicate customers. As the customers are getting created twice on the Stripe Dashboard, the issue is not with dj-stripe as we just sync to the local db and do not create anything on the Stripe Dashboard.

I think some other part of your code is somehow triggering something that is resulting in customer creation because one of your customers has a description and one doesn't so I'm inclined to say that the Stripe Checkout is creating one customer.

Do both these customers have the same metadata? The one created by Checkout session should have the subscriber key in the metadata dict.

alecdalelio commented 2 years ago

@arnav13081994 Only the customer with no description has metadata. It has the following: djstripe_subscriber: 52

The other customer whose description is "Alec DAlelio" is actually the customer that I want...that is the one containing the payment method, subscription plan, etc.

alecdalelio commented 2 years ago

Worth noting that both of these customers are being created in my local db each time.

arnav13081994 commented 2 years ago

The customer with djstripe_subscriber key in its metadata dict is the one created by Stripe Checkout.

Are you populating the field corresponding to "Alec DAlelio" somewhere else in your code? Perhaps that's the name or the full_name field or something?

They will be in your local db as they are on the Stripe Dashboard. dj-stripe syncs whatever data is in the upstream Stripe Dashboard. Had there been any discrepancy between the two, that would have been a sureshot indication of some issue with dj-stripe. You can also experiment/confirm this is indeed the case by stopping the local stripe server that is forwarding webhooks on your local. There should still be 2 customers on the Stripe Dashboard.

Also one more thing worth trying would be to update to 2.5.1 and repeating this.

alecdalelio commented 2 years ago

The field corresponding to "Alec DAlelio" here comes from the Stripe Checkout session. The user that I am testing this on has a separate name in my db/in the myApp.User model. Here it is again after going through the flow once more using your name instead in the "Name on Card" field.

Screen Shot 2021-11-01 at 12 03 25 AM

Do you have any recommendation as to how I might address this? I am in the same predicament I have been in for weeks since the issue was opened and don't really know what more to do in order to fix this. Feels like an impasse.

arnav13081994 commented 2 years ago

Wait. You just mentioned that the customer with no description had the 'djstripe_subscriber' key but now you are saying the customer with the populated description is coming from Stripe Checkout. I'm confused. The one from stripe checkout created by the APIView must have that key.

As it stands right now, there doesn't seem to be any issue with Dj-stripe, the library itself. And I am also out of ideas about what could go wrong.

As a last resort, I can offer to go through your code, if that's something you are comfortable with.

alecdalelio commented 2 years ago

I believe both customers are coming from Stripe Checkout. The bottom one for whatever reason is taking the "Name on card" input as the description. That same Stripe customer also includes all of the data that I want (payment method, subscription, etc.)

The second customer (on top) is the one that includes the djstripe_subscriber metadata. However, that is all that it includes. It doesn't have any of the data from the Stripe checkout process. In other words, it appears to be unnecessary. If I can prevent this second customer from being created at all then it should work fine. The issue is pinpointing why the second customer is being created at all.

alecdalelio commented 2 years ago

I think I see what's happening but still do not understand why.

The customer that is being created via Stripe Checkout is not actually being associated with djstripe_subscriber_model at all. One customer (with the description) is taking all of the data from the Stripe Checkout session. The other customer is completely empty with the exception of the metadata from the API view. I need to consolidate this data so that the Stripe Checkout customer contains all of said data AND the metadata without creating an additional empty customer object.

arnav13081994 commented 2 years ago

You would need to update to 2.5.1 and then override the files as done in #1416 or wait until the next minor release. That way the created customer from checkout will get associated with the User as specified by djstripe_subscriber key in the metadata dict.

Also it's unlikely that there is any issue with the dj-stripe source code that is causing duplicate customers to be created because:

1) The code you have shared so far literally does not use dj-stripe or any of its methods. 2) dj-stripe only syncs data to your local db depending on which webhook returned what data. If the data exists on your Stripe Account, then it will also be in your local db.

alecdalelio commented 2 years ago

@arnav13081994 Thanks for clarifying. I've confirmed I am using 2.5.1 already. Could you please specify exactly what you mean by "override the files as done" in the PR? Which files am I overriding and where? Thanks so much - I think this is nearly all sorted out.

alecdalelio commented 2 years ago

Implementing the checkout webhook from PR #1416 mostly worked - it stopped the second customer from being created. However, the customer that WAS created does not have any metadata, in other words it is not linked to myApp.user / DJSTRIPE_SUBSCRIBER_MODEL yet.

It is still not fully compatible with my integration but is a step in the right direction. It is pretty crucial that I can keep my Stripe customer linked to myApp.user because so much of my frontend implementation relies on it.

alecdalelio commented 2 years ago

@arnav13081994 as you requested via Discord - here is the updated code including:

I am using the CustomerPortalApiView as a signal as to whether or not this is working. Currently on my Stripe dashboard I can see that only one customer is being created - this is great progress! Unfortunately, that customer does NOT contain the metadata / subscriber ID that it needs to relate to myApp.user model.

class CheckoutSessionApiView(APIView):
    """
    Checkout view for membership
    """

    # @limits(RATE_LIMIT_REQUESTS, RATE_LIMIT_DURATION)
    def post(self, request):
        """
        Create Stripe checkout session
        """

        id = request.user.id 
        metadata = {f"{djstripe_settings.SUBSCRIBER_CUSTOMER_KEY}": id}

        try:

            session = stripe.checkout.Session.create(
                payment_method_types=["card"],
                line_items=[{"price": request.data["price_id"], "quantity": 1}],
                mode="subscription",
                client_reference_id=request.user.id,
                success_url=FRONTEND_URL + "/profile/" + str(request.user.id),
                cancel_url=FRONTEND_URL,
                customer_email=request.user.email,
                metadata=metadata
            )

            return Response({"session_id": session.id}, status=status.HTTP_200_OK)
        except ValueError:
            return Response(status=status.HTTP_404_NOT_FOUND)

class CustomerPortalApiView(APIView):
    def get(self, request):
        subscriber = Customer.objects.get(subscriber=request.user)
        session = stripe.billing_portal.Session.create(customer=subscriber.id, return_url="https://myapp.com")
        return Response({"redirect_url": session.url}, status=status.HTTP_200_OK)

@webhooks.handler("checkout")
def checkout_webhook_handler(event):
    metadata = event.data.get("object", {}).get("metadata", {})
    customer_id = event.data.get("object", {}).get("customer", "")
    subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY

    # only update customer.subscriber if both the customer and subscriber already exist
    update_customer_helper(metadata, customer_id, subscriber_key)

    _handle_crud_like_event(target_cls=Session, event=event)

def update_customer_helper(metadata, customer_id, subscriber_key):
    """
    A helper function that updates customer's subscriber and metadata fields
    """

    # only update customer.subscriber if both the customer and subscriber already exist
    if (
        subscriber_key not in ("", None)
        and metadata.get(subscriber_key, "")
        and customer_id
    ):
        try:
            subscriber = djstripe_settings.get_subscriber_model().objects.get(
                id=metadata.get(subscriber_key, "")
            )
            customer = models.Customer.objects.get(id=customer_id)
            customer.subscriber = subscriber
            customer.metadata = metadata
            customer.save()

        except ObjectDoesNotExist:
            pass
arnav13081994 commented 2 years ago

@alecdalelio Thank you for posting the upto date code.

I suspect the customer.created event is fired after the checkout.session.completed as the order of the webhooks is not reliable anyway and that's why the metadata is not being updated, which is expected behaviour as the customer doesn't exist yet. And that's also why update_customer_helper(metadata, customer_id, subscriber_key) is invoked in the customer.created webhook as well in the source code.

Add this snippet to your code.


@webhooks.handler("customer")
def customer_webhook_handler(event):
    """Handle updates to customer objects.

    First determines the crud_type and then handles the event if a customer
    exists locally.
    As customers are tied to local users, djstripe will not create customers that
    do not already exist locally.

    And updates to the subscriber model and metadata fields of customer if present
    in checkout.sessions metadata key.

    Docs and an example customer webhook response:
    https://stripe.com/docs/api#customer_object
    """
    # will recieve all events of the type customer.X.Y so
    # need to ensure the data object is related to Customer Object
    target_object_type = event.data.get("object", {}).get("object", {})

    if event.customer and target_object_type == "customer":

        metadata = event.data.get("object", {}).get("metadata", {})
        customer_id = event.data.get("object", {}).get("id", "")
        subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY

        # only update customer.subscriber if both the customer and subscriber already exist
        update_customer_helper(metadata, customer_id, subscriber_key)

        _handle_crud_like_event(target_cls=models.Customer, event=event)
alecdalelio commented 2 years ago

Thanks for the insight, @arnav13081994.

I've added this webhook listener and unfortunately the behavior has not changed at all.

alecdalelio commented 2 years ago

@arnav13081994 Here's views.py including a webhook event listener that I neglected to include before. It could be that there's a conflict between my webhook handler and the one that you had me implement from dj-stripe.

class CheckoutSessionApiView(APIView):
    """
    Checkout view for membership
    """

    # @limits(RATE_LIMIT_REQUESTS, RATE_LIMIT_DURATION)
    def post(self, request):
        """
        Create Stripe checkout session
        """

        id = request.user.id 
        metadata = {f"{djstripe_settings.SUBSCRIBER_CUSTOMER_KEY}": id}

        try:

            session = stripe.checkout.Session.create(
                payment_method_types=["card"],
                line_items=[{"price": request.data["price_id"], "quantity": 1}],
                mode="subscription",
                client_reference_id=request.user.id,
                success_url=FRONTEND_URL + "/profile/" + str(request.user.id),
                cancel_url=FRONTEND_URL,
                customer_email=request.user.email,
                metadata=metadata
            )

            return Response({"session_id": session.id}, status=status.HTTP_200_OK)
        except ValueError:
            return Response(status=status.HTTP_404_NOT_FOUND)

class CustomerPortalApiView(APIView):
    def get(self, request):
        subscriber = Customer.objects.get(subscriber=request.user)
        session = stripe.billing_portal.Session.create(customer=subscriber.id, return_url="https://mochi.game")
        return Response({"redirect_url": session.url}, status=status.HTTP_200_OK)

@webhooks.handler("customer")
def customer_webhook_handler(event):
    """Handle updates to customer objects.

    First determines the crud_type and then handles the event if a customer
    exists locally.
    As customers are tied to local users, djstripe will not create customers that
    do not already exist locally.

    And updates to the subscriber model and metadata fields of customer if present
    in checkout.sessions metadata key.

    Docs and an example customer webhook response:
    https://stripe.com/docs/api#customer_object
    """
    # will recieve all events of the type customer.X.Y so
    # need to ensure the data object is related to Customer Object
    target_object_type = event.data.get("object", {}).get("object", {})

    if event.customer and target_object_type == "customer":

        metadata = event.data.get("object", {}).get("metadata", {})
        customer_id = event.data.get("object", {}).get("id", "")
        subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY

        # only update customer.subscriber if both the customer and subscriber already exist
        update_customer_helper(metadata, customer_id, subscriber_key)

        _handle_crud_like_event(target_cls=models.Customer, event=event)

@webhooks.handler("checkout")
def checkout_webhook_handler(event):
    """
    Handle updates to Checkout Session objects
    And updates to the subscriber model and metadata fields of customer if present
    in checkout.sessions metadata key.
    Please note djstripe doesn't create new subscriber and customer instances
    - checkout: https://stripe.com/docs/api/checkout/sessions
    """
    metadata = event.data.get("object", {}).get("metadata", {})
    customer_id = event.data.get("object", {}).get("customer", "")
    subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY

    # only update customer.subscriber if both the customer and subscriber already exist
    update_customer_helper(metadata, customer_id, subscriber_key)

    _handle_crud_like_event(target_cls=Session, event=event)

@require_POST
@csrf_exempt
@webhooks.handler("checkout.session.completed")
def checkout_success(event, **kwargs):
    session = event.data["object"]

    # Fetch test data from session
    try:
        customer_email = session["customer_details"]["email"]
        user = User.objects.filter(email=customer_email).first()
        if user:
            Customer.get_or_create(subscriber=user)

        return HttpResponse(status=status.HTTP_200_OK)
    except ValueError:
        return Response(status=status.HTTP_404_NOT_FOUND)

def update_customer_helper(metadata, customer_id, subscriber_key):
    """
    A helper function that updates customer's subscriber and metadata fields
    """

    # only update customer.subscriber if both the customer and subscriber already exist
    if (
        subscriber_key not in ("", None)
        and metadata.get(subscriber_key, "")
        and customer_id
    ):
        try:
            subscriber = djstripe_settings.get_subscriber_model().objects.get(
                id=metadata.get(subscriber_key, "")
            )
            customer = models.Customer.objects.get(id=customer_id)
            customer.subscriber = subscriber
            customer.metadata = metadata
            customer.save()

        except ObjectDoesNotExist:
            pass
arnav13081994 commented 2 years ago

@alecdalelio Can you remove the webhook mentioned below


@require_POST
@csrf_exempt
@webhooks.handler("checkout.session.completed")
def checkout_success(event, **kwargs):
    session = event.data["object"]

    # Fetch test data from session
    try:
        customer_email = session["customer_details"]["email"]
        user = User.objects.filter(email=customer_email).first()
        if user:
            Customer.get_or_create(subscriber=user)

        return HttpResponse(status=status.HTTP_200_OK)
    except ValueError:
        return Response(status=status.HTTP_404_NOT_FOUND)
alecdalelio commented 2 years ago

@arnav13081994 That was my first impulse - unfortunately removing it did not change anything

arnav13081994 commented 2 years ago

Can you run python manage.py djstripe_sync_models Customer Session? And check if the customer metdata gets updated and the relation between the Customer and the Subscriber gets created?

Also can you also add a few print statement to see how update_customer_helper(metadata, customer_id, subscriber_key) is getting invoked?

alecdalelio commented 2 years ago

Good ideas - yeah I'll give that a go and let you know how it goes.

alecdalelio commented 2 years ago

@arnav13081994 I'm getting this: NameError: name '_handle_crud_like_event' is not defined

I am importing it like so: from djstripe.event_handlers import *

Would it be better if I actually copy/paste that method from dj-stripe source code into my views.py, import it directly, or something different?

arnav13081994 commented 2 years ago

You'll have to copy paste it as it is in master and not 2.5.1

alecdalelio commented 2 years ago

File "/Users/alecdalelio/Development/Mochi/backend/payments/views.py", line 259, in _handle_crud_like_event if crud_type is CrudType.DELETED: AttributeError: type object 'CrudType' has no attribute 'DELETED'

This error seems to be preventing the webhooks from running properly

arnav13081994 commented 2 years ago

@alecdalelio Let me try to run a few scenarios on my end. I suspect, the customer does not exist locally by the time the 2 webhooks get fired.

For now, You can remove all the 3 webhooks (checkout_success, customer_webhook_handler, checkout_webhook_handler), and instead add this

@webhooks.handler("customer.created")
def add_customer_subscriber(event, **kwargs):
    """
    Function that adds the customer.subscriber attribute to the
    Customer Object
    """
    print("🔔 Customer Created on Stripe")
    session = event.data["object"]

    email = session["email"]
     # in case more than 1 user exists with the same email, we query the first created user. SHOULD NOT HAPPEN THOUGH
    user = User.objects.filter(email=email).first()

    if user:
        # in case more than 1 customer exists with the same email, we query the latest created customer. SHOULD NOT HAPPEN THOUGH
        customer = Customer.objects.filter(email=email).last()
        customer.subscriber = user
        customer.save()

This completely bypasses whether metadata is in checkout or not. I assume User model is the Subscriber you want your Customer to get associated with and a User can be uniquely identified with their email.

alecdalelio commented 2 years ago

Sounds like a safe assumption regarding the Webhooks. You're correct - User is the Subscriber model and I set up the Checkout session such that User.email is pre-loaded when the user redirects to checkout. So I can guarantee that Customer.email and User.email are consistent.

arnav13081994 commented 2 years ago

Ok. Please let me know if this fixes the 2 issues or not.

alecdalelio commented 2 years ago

These changes did not have an impact on the behavior - still no metadata on the customer.

arnav13081994 commented 2 years ago

Oh sorry I forgot to add a line for metdata. But you are getting the correct association between the User and the Customer and no duplicate customers?

@webhooks.handler("customer.created")
def add_customer_subscriber(event, **kwargs):
    """
    Function that adds the customer.subscriber attribute to the
    Customer Object
    """
    print("🔔 Customer Created on Stripe")
    session = event.data["object"]

    email = session["email"]
     # in case more than 1 user exists with the same email, we query the first created user. SHOULD NOT HAPPEN THOUGH
    user = User.objects.filter(email=email).first()

    if user:
        # in case more than 1 customer exists with the same email, we query the latest created customer. SHOULD NOT HAPPEN THOUGH
        customer = Customer.objects.filter(email=email).last()
        customer.subscriber = user
        customer.metadata = {"djstripe_subscriber": user.id}
        customer.save()
alecdalelio commented 2 years ago

Gotcha - I'm going into a quick meeting right now but I'll try it again with the metadata line in ~30min!

alecdalelio commented 2 years ago

@arnav13081994 sorry for the delay - even with the update to add_customer_subscriber() I'm still getting exactly the same behavior

arnav13081994 commented 2 years ago

@alecdalelio Just to be clear you're getting 1 customer with no metadata and no relation to User or you're getting duplicate customers and no relation between the Customer and User?

alecdalelio commented 2 years ago

The former- I'm getting one customer via Stripe Checkout which has no metadata and no relation to the user. It does have all the relevant payment/subscription info, though.

arnav13081994 commented 2 years ago

@alecdalelio I'm sorry to hear that. I think I have been able to fix the issue.

Please remove the webhook, checkout_success.

Keep the customer_webhook_handler webhook and update the checkout_webhook_handler like this:

@webhooks.handler("checkout")
def checkout_webhook_handler(event):
    """
    Handle updates to Checkout Session objects
    And updates to the subscriber model and metadata fields of customer if present
    in checkout.sessions metadata key.

    Please note djstripe doesn't create new subscriber and customer instances
    - checkout: https://stripe.com/docs/api/checkout/sessions
    """
    metadata = event.data.get("object", {}).get("metadata", {})
    customer_id = event.data.get("object", {}).get("customer", "")
    subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY

    _handle_crud_like_event(target_cls=models.Session, event=event)

    # only update customer.subscriber if both the customer and subscriber already exist
    update_customer_helper(metadata, customer_id, subscriber_key)

Basically I moved update_customer_helper(metadata, customer_id, subscriber_key) to below _handle_crud_like_event(target_cls=models.Session, event=event)

arnav13081994 commented 2 years ago

@alecdalelio I have found the root cause. I will raise a PR in the next few hours that should fix it.

alecdalelio commented 2 years ago

@arnav13081994 Excellent! I'll be traveling today but I'll let you know as soon as I'm able to look it over and implement it.

arnav13081994 commented 2 years ago

@alecdalelio Please refer to this PR #1471

arnav13081994 commented 2 years ago

@alecdalelio I was wondering if you got a chance to check the PR out.

alecdalelio commented 2 years ago

Hey @arnav13081994 - I've taken a look at the PR and seems to make sense. Was waiting on y'all to pull it into the codebase so I can test it out. Anything you need from me to help move that process along?

arnav13081994 commented 2 years ago

Yes that would make more sense. I forgot there are changes in the models too and that would not be possible for you integrate into your code.

alecdalelio commented 2 years ago

Hey @arnav13081994 I see this PR is still open - any progress on your end? Hoping to figure out a timeline for when I can fix my Stripe integration so that I can communicate to my customers. Let me know!

arnav13081994 commented 2 years ago

Hi @alecdalelio I am waiting for the PR to get merged too. I have no idea why it is not yet merged.

alecdalelio commented 2 years ago

Hey @arnav13081994! Could you touch base with your collaborators here regarding this PR?

arnav13081994 commented 2 years ago

Hi @alecdalelio We are planning on releasing a new version by the end of this month, if not sooner. Sorry for the delay.