laravel / cashier-stripe

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.
https://laravel.com/docs/billing
MIT License
2.37k stars 670 forks source link

Unsubscribe field ends_at is null #1632

Closed GuhLucena closed 8 months ago

GuhLucena commented 8 months ago

Cashier Stripe Version

15.1

Laravel Version

10.10

PHP Version

8.2

Database Driver & Version

MariaDB 10.4.32

Description

When canceling a subscription that is configured to be canceled immediately and responds to the collection reasson, the 'ends_at' field in the subscriptions table is set to null.

And $request->user()?->subscribed() does not identify that the subscription has ended

Steps To Reproduce

The subscription is set to be canceled immediately and with the Cancellation reason activated on the Customer portal. https://dashboard.stripe.com/test/settings/billing/portal

image

when the customer cancels the subscription via the portal and replies to "Cancellation reason", the 'ends_at' field is set to 'null'

image

The problem is in the webhook it is deleting the 'ends_at' value because the update event is executed after the delete event. image

driesvints commented 8 months ago

Hi @GuhLucena. Could you share the payloads of these two events?

GuhLucena commented 8 months ago

customer.subscription.updated

{
  "id": ["evt_1OX6sPAjJBizF0r1YxfNjZgL"](https://dashboard.stripe.com/test/events/evt_1OX6sPAjJBizF0r1YxfNjZgL),
  "object": "event",
  "api_version": "2023-10-16",
  "created": 1704912705,
  "data": {
    "object": {
      "id": ["sub_1OX6qWAjJBizF0r1Qiuceeyl"](https://dashboard.stripe.com/test/subscriptions/sub_1OX6qWAjJBizF0r1Qiuceeyl),
      "object": "subscription",
      "application": null,
      "application_fee_percent": null,
      "automatic_tax": {
        "enabled": false
      },
      "billing_cycle_anchor": 1704912588,
      "billing_thresholds": null,
      "cancel_at": null,
      "cancel_at_period_end": false,
      "canceled_at": 1704912692,
      "cancellation_details": {
        "comment": "CARAO DIMAIIIIIS",
        "feedback": "too_expensive",
        "reason": "cancellation_requested"
      },
      "collection_method": "charge_automatically",
      "created": 1704912588,
      "currency": "brl",
      "current_period_end": 1707590988,
      "current_period_start": 1704912588,
      "customer": ["cus_PLoRuG6bEwluyH"](https://dashboard.stripe.com/test/customers/cus_PLoRuG6bEwluyH),
      "days_until_due": null,
      "default_payment_method": "pm_1OX6qVAjJBizF0r1MNGj7UsB",
      "default_source": null,
      "default_tax_rates": [
      ],
      "description": null,
      "discount": null,
      "ended_at": 1704912692,
      "items": {
        "object": "list",
        "data": [
          {
            "id": ["si_PLoT6tl9dX7zVl"](https://dashboard.stripe.com/test),
            "object": "subscription_item",
            "billing_thresholds": null,
            "created": 1704912589,
            "metadata": {
            },
            "plan": {
              "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
              "object": "plan",
              "active": true,
              "aggregate_usage": null,
              "amount": 500,
              "amount_decimal": "500",
              "billing_scheme": "per_unit",
              "created": 1704498878,
              "currency": "brl",
              "interval": "month",
              "interval_count": 1,
              "livemode": false,
              "metadata": {
              },
              "nickname": null,
              "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
              "tiers_mode": null,
              "transform_usage": null,
              "trial_period_days": null,
              "usage_type": "licensed"
            },
            "price": {
              "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
              "object": "price",
              "active": true,
              "billing_scheme": "per_unit",
              "created": 1704498878,
              "currency": "brl",
              "custom_unit_amount": null,
              "livemode": false,
              "lookup_key": null,
              "metadata": {
              },
              "nickname": null,
              "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
              "recurring": {
                "aggregate_usage": null,
                "interval": "month",
                "interval_count": 1,
                "trial_period_days": null,
                "usage_type": "licensed"
              },
              "tax_behavior": "unspecified",
              "tiers_mode": null,
              "transform_quantity": null,
              "type": "recurring",
              "unit_amount": 500,
              "unit_amount_decimal": "500"
            },
            "quantity": 1,
            "subscription": ["sub_1OX6qWAjJBizF0r1Qiuceeyl"](https://dashboard.stripe.com/test/subscriptions/sub_1OX6qWAjJBizF0r1Qiuceeyl),
            "tax_rates": [
            ]
          }
        ],
        "has_more": false,
        "total_count": 1,
        "url": "/v1/subscription_items?subscription=sub_1OX6qWAjJBizF0r1Qiuceeyl"
      },
      "latest_invoice": "in_1OX6qWAjJBizF0r1PWWs9MAO",
      "livemode": false,
      "metadata": {
        "is_on_session_checkout": "true",
        "type": "default",
        "name": "default"
      },
      "next_pending_invoice_item_invoice": null,
      "on_behalf_of": null,
      "pause_collection": null,
      "payment_settings": {
        "payment_method_options": null,
        "payment_method_types": null,
        "save_default_payment_method": "off"
      },
      "pending_invoice_item_interval": null,
      "pending_setup_intent": null,
      "pending_update": null,
      "plan": {
        "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
        "object": "plan",
        "active": true,
        "aggregate_usage": null,
        "amount": 500,
        "amount_decimal": "500",
        "billing_scheme": "per_unit",
        "created": 1704498878,
        "currency": "brl",
        "interval": "month",
        "interval_count": 1,
        "livemode": false,
        "metadata": {
        },
        "nickname": null,
        "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
        "tiers_mode": null,
        "transform_usage": null,
        "trial_period_days": null,
        "usage_type": "licensed"
      },
      "quantity": 1,
      "schedule": null,
      "start_date": 1704912588,
      "status": "canceled",
      "test_clock": null,
      "transfer_data": null,
      "trial_end": null,
      "trial_settings": {
        "end_behavior": {
          "missing_payment_method": "create_invoice"
        }
      },
      "trial_start": null
    },
    "previous_attributes": {
      "cancellation_details": {
        "comment": null,
        "feedback": null
      }
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_AOeIlKkjAZkCLU",
    "idempotency_key": "7b20dd47-c44c-4eb0-8111-bed1109dee47"
  },
  "type": "customer.subscription.updated"
}

customer.subscription.deleted

{
  "id": ["evt_1OX6sDAjJBizF0r1lecNHf8e"](https://dashboard.stripe.com/test/events/evt_1OX6sDAjJBizF0r1lecNHf8e),
  "object": "event",
  "api_version": "2023-10-16",
  "created": 1704912693,
  "data": {
    "object": {
      "id": ["sub_1OX6qWAjJBizF0r1Qiuceeyl"](https://dashboard.stripe.com/test/subscriptions/sub_1OX6qWAjJBizF0r1Qiuceeyl),
      "object": "subscription",
      "application": null,
      "application_fee_percent": null,
      "automatic_tax": {
        "enabled": false
      },
      "billing_cycle_anchor": 1704912588,
      "billing_thresholds": null,
      "cancel_at": null,
      "cancel_at_period_end": false,
      "canceled_at": 1704912692,
      "cancellation_details": {
        "comment": null,
        "feedback": null,
        "reason": "cancellation_requested"
      },
      "collection_method": "charge_automatically",
      "created": 1704912588,
      "currency": "brl",
      "current_period_end": 1707590988,
      "current_period_start": 1704912588,
      "customer": ["cus_PLoRuG6bEwluyH"](https://dashboard.stripe.com/test/customers/cus_PLoRuG6bEwluyH),
      "days_until_due": null,
      "default_payment_method": "pm_1OX6qVAjJBizF0r1MNGj7UsB",
      "default_source": null,
      "default_tax_rates": [
      ],
      "description": null,
      "discount": null,
      "ended_at": 1704912692,
      "items": {
        "object": "list",
        "data": [
          {
            "id": ["si_PLoT6tl9dX7zVl"](https://dashboard.stripe.com/test),
            "object": "subscription_item",
            "billing_thresholds": null,
            "created": 1704912589,
            "metadata": {
            },
            "plan": {
              "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
              "object": "plan",
              "active": true,
              "aggregate_usage": null,
              "amount": 500,
              "amount_decimal": "500",
              "billing_scheme": "per_unit",
              "created": 1704498878,
              "currency": "brl",
              "interval": "month",
              "interval_count": 1,
              "livemode": false,
              "metadata": {
              },
              "nickname": null,
              "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
              "tiers_mode": null,
              "transform_usage": null,
              "trial_period_days": null,
              "usage_type": "licensed"
            },
            "price": {
              "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
              "object": "price",
              "active": true,
              "billing_scheme": "per_unit",
              "created": 1704498878,
              "currency": "brl",
              "custom_unit_amount": null,
              "livemode": false,
              "lookup_key": null,
              "metadata": {
              },
              "nickname": null,
              "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
              "recurring": {
                "aggregate_usage": null,
                "interval": "month",
                "interval_count": 1,
                "trial_period_days": null,
                "usage_type": "licensed"
              },
              "tax_behavior": "unspecified",
              "tiers_mode": null,
              "transform_quantity": null,
              "type": "recurring",
              "unit_amount": 500,
              "unit_amount_decimal": "500"
            },
            "quantity": 1,
            "subscription": ["sub_1OX6qWAjJBizF0r1Qiuceeyl"](https://dashboard.stripe.com/test/subscriptions/sub_1OX6qWAjJBizF0r1Qiuceeyl),
            "tax_rates": [
            ]
          }
        ],
        "has_more": false,
        "total_count": 1,
        "url": "/v1/subscription_items?subscription=sub_1OX6qWAjJBizF0r1Qiuceeyl"
      },
      "latest_invoice": "in_1OX6qWAjJBizF0r1PWWs9MAO",
      "livemode": false,
      "metadata": {
        "is_on_session_checkout": "true",
        "type": "default",
        "name": "default"
      },
      "next_pending_invoice_item_invoice": null,
      "on_behalf_of": null,
      "pause_collection": null,
      "payment_settings": {
        "payment_method_options": null,
        "payment_method_types": null,
        "save_default_payment_method": "off"
      },
      "pending_invoice_item_interval": null,
      "pending_setup_intent": null,
      "pending_update": null,
      "plan": {
        "id": ["price_1OVNDmAjJBizF0r1zMsNhxKW"](https://dashboard.stripe.com/test/prices/price_1OVNDmAjJBizF0r1zMsNhxKW),
        "object": "plan",
        "active": true,
        "aggregate_usage": null,
        "amount": 500,
        "amount_decimal": "500",
        "billing_scheme": "per_unit",
        "created": 1704498878,
        "currency": "brl",
        "interval": "month",
        "interval_count": 1,
        "livemode": false,
        "metadata": {
        },
        "nickname": null,
        "product": ["prod_PK1GhuGcYvoaKV"](https://dashboard.stripe.com/test/products/prod_PK1GhuGcYvoaKV),
        "tiers_mode": null,
        "transform_usage": null,
        "trial_period_days": null,
        "usage_type": "licensed"
      },
      "quantity": 1,
      "schedule": null,
      "start_date": 1704912588,
      "status": "canceled",
      "test_clock": null,
      "transfer_data": null,
      "trial_end": null,
      "trial_settings": {
        "end_behavior": {
          "missing_payment_method": "create_invoice"
        }
      },
      "trial_start": null
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": null,
    "idempotency_key": "c15079ab-8dd4-4c9f-accc-ffb8e65d9828"
  },
  "type": "customer.subscription.deleted"
}
GuhLucena commented 8 months ago

Maybe this will give you more information.

when you cancel without replying and commenting, the order of the events in the webhook looks like the one circled in blue.

and when you cancel, reply and comment, it looks like the one circled in orange.

image

GuhLucena commented 8 months ago

image

I think I've found it!

instead of "cancel_at" the right one is "canceled_at"

GuhLucena commented 8 months ago

If you look deeper, there is: 'cancel_at' => date on which it will be canceled when this is configured as "cancel at end of billing period" image

'canceled_at' => the date it was canceled. If it is set to 'cancel immediately' it will have the timestamp and 'cancel_at' will be null. image

In this case, you need to check whether the 'cancel_at_period_end' field is true or false. true -> use cancel_at false -> use cancel_at if different from null

driesvints commented 8 months ago

Thank you. I sent in https://github.com/laravel/cashier-stripe/pull/1633 for this.