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 667 forks source link

trial_ends_at is getting marked as NULL when webhook is received #1673

Closed sts-ryan-holton closed 3 months ago

sts-ryan-holton commented 3 months ago

Cashier Stripe Version

15.3.2

Laravel Version

11.4.0

PHP Version

8.3.4

Database Driver & Version

MysQL 8

Description

Hi :wave: I've just observed some worrying behaviour with the trial_ends_at column with subscription trials. I've got some kind that creates a new subscription with trialUntil method which I can see in my Stripe dashboard and billing portal that it's correct. I then update trial_ends_at with this date, and when the webhook is received, it marks the column as null.

Worringly, I've checked my production database and found that it's been doing this since around the end of January 2024 and wasn't doing it before this as over 300 customers have got the correct value.

The following successfully marks trial_ends_at:

$trialPeriod = Carbon::now()->addYears(1);

$user->is_lifetime = true;
$user->trial_ends_at = $trialPeriod;
$user->save();

$user->newSubscription('default', $plan->stripe_id)
    ->trialUntil($trialPeriod)
    ->create();

But I've verified that it is the webhook since I stopped my Laravel Share expose URL and all was good, but when starting it again and re-attempting it gets reset.

Steps To Reproduce

  1. Mark trial_ends_at and call trialUntil
  2. Webhook is received and trial_ends_at on User model gets set to NULL

I managed to get the three Webhooks that were received in my application and handled by Cashier:

These JSON outputs are from my test Stripe account in test mode

customer.updated ```json { "event":{ "Laravel\\Cashier\\Events\\WebhookReceived":{ "payload":{ "id":"evt_1PAdMrHQLg2q8v4mNMawyUeO", "object":"event", "api_version":"2023-10-16", "created":1714332393, "data":{ "object":{ "id":"cus_Q0egD21BpeM2h1", "object":"customer", "address":null, "balance":0, "created":1714332392, "currency":"gbp", "default_source":null, "delinquent":false, "description":null, "discount":null, "email":"123123@gmail.com", "invoice_prefix":"1DE555F2", "invoice_settings":{ "custom_fields":null, "default_payment_method":null, "footer":null, "rendering_options":null }, "livemode":false, "metadata":[ ], "name":"123123", "phone":null, "preferred_locales":[ ], "shipping":null, "tax_exempt":"none", "test_clock":null }, "previous_attributes":{ "currency":null } }, "livemode":false, "pending_webhooks":1, "request":{ "id":"req_zLokxMgCAsYwfR", "idempotency_key":"8f55d877-a1a8-4ea2-8502-5f489c88496c" }, "type":"customer.updated" } } } } ```
invoice.payment_succeeded ```json { "event":{ "Laravel\\Cashier\\Events\\WebhookReceived":{ "payload":{ "id":"evt_1PAdMsHQLg2q8v4m9Fe5yUA6", "object":"event", "api_version":"2023-10-16", "created":1714332393, "data":{ "object":{ "id":"in_1PAdMrHQLg2q8v4mG3KZ1Z6f", "object":"invoice", "account_country":"GB", "account_name":"testtest", "account_tax_ids":null, "amount_due":0, "amount_paid":0, "amount_remaining":0, "amount_shipping":0, "application":null, "application_fee_amount":null, "attempt_count":0, "attempted":true, "auto_advance":false, "automatic_tax":{ "enabled":false, "liability":null, "status":null }, "billing_reason":"subscription_create", "charge":null, "collection_method":"charge_automatically", "created":1714332393, "currency":"gbp", "custom_fields":null, "customer":"cus_Q0egD21BpeM2h1", "customer_address":null, "customer_email":"123123@gmail.com", "customer_name":"123123", "customer_phone":null, "customer_shipping":null, "customer_tax_exempt":"none", "customer_tax_ids":[ ], "default_payment_method":null, "default_source":null, "default_tax_rates":[ ], "description":null, "discount":null, "discounts":[ ], "due_date":null, "effective_at":1714332393, "ending_balance":0, "footer":null, "from_invoice":null, "hosted_invoice_url":"https://invoice.stripe.com/i/acct_1JLoDZHQLg2q8v4m/test_YWNjdF8xSkxvRFpIUUxnMnE4djRtLF9RMGVnNUVsRXg2R3RhdWJvcUw4NGlVOGZwQXVtOXZ4LDEwNDg3MzE5NA0200SmnSDYR7?s=ap", "invoice_pdf":"https://pay.stripe.com/invoice/acct_1JLoDZHQLg2q8v4m/test_YWNjdF8xSkxvRFpIUUxnMnE4djRtLF9RMGVnNUVsRXg2R3RhdWJvcUw4NGlVOGZwQXVtOXZ4LDEwNDg3MzE5NA0200SmnSDYR7/pdf?s=ap", "issuer":{ "type":"self" }, "last_finalization_error":null, "latest_revision":null, "lines":{ "object":"list", "data":[ { "id":"il_1PAdMrHQLg2q8v4mLv9vblht", "object":"line_item", "amount":0, "amount_excluding_tax":0, "currency":"gbp", "description":"Trial period for Monthly (75 monitors, 75 domains)", "discount_amounts":[ ], "discountable":true, "discounts":[ ], "invoice":"in_1PAdMrHQLg2q8v4mG3KZ1Z6f", "livemode":false, "metadata":[ ], "period":{ "end":1745868392, "start":1714332393 }, "plan":{ "id":"price_1NP3ytHQLg2q8v4mMJuTnKJ8", "object":"plan", "active":true, "aggregate_usage":null, "amount":799, "amount_decimal":"799", "billing_scheme":"per_unit", "created":1688218615, "currency":"gbp", "interval":"month", "interval_count":1, "livemode":false, "metadata":[ ], "meter":null, "nickname":null, "product":"prod_OBQqH7pzxYHPQM", "tiers_mode":null, "transform_usage":null, "trial_period_days":null, "usage_type":"licensed" }, "price":{ "id":"price_1NP3ytHQLg2q8v4mMJuTnKJ8", "object":"price", "active":true, "billing_scheme":"per_unit", "created":1688218615, "currency":"gbp", "custom_unit_amount":null, "livemode":false, "lookup_key":null, "metadata":[ ], "nickname":null, "product":"prod_OBQqH7pzxYHPQM", "recurring":{ "aggregate_usage":null, "interval":"month", "interval_count":1, "meter":null, "trial_period_days":null, "usage_type":"licensed" }, "tax_behavior":"unspecified", "tiers_mode":null, "transform_quantity":null, "type":"recurring", "unit_amount":799, "unit_amount_decimal":"799" }, "proration":false, "proration_details":{ "credited_items":null }, "quantity":1, "subscription":"sub_1PAdMrHQLg2q8v4mZCFRc5aq", "subscription_item":"si_Q0egfNnZYUWptv", "tax_amounts":[ ], "tax_rates":[ ], "type":"subscription", "unit_amount_excluding_tax":"0" } ], "has_more":false, "total_count":1, "url":"/v1/invoices/in_1PAdMrHQLg2q8v4mG3KZ1Z6f/lines" }, "livemode":false, "metadata":[ ], "next_payment_attempt":null, "number":"1F3626C5-0311", "on_behalf_of":null, "paid":true, "paid_out_of_band":false, "payment_intent":null, "payment_settings":{ "default_mandate":null, "payment_method_options":null, "payment_method_types":null }, "period_end":1714332393, "period_start":1714332393, "post_payment_credit_notes_amount":0, "pre_payment_credit_notes_amount":0, "quote":null, "receipt_number":null, "rendering":null, "rendering_options":null, "shipping_cost":null, "shipping_details":null, "starting_balance":0, "statement_descriptor":null, "status":"paid", "status_transitions":{ "finalized_at":1714332393, "marked_uncollectible_at":null, "paid_at":1714332393, "voided_at":null }, "subscription":"sub_1PAdMrHQLg2q8v4mZCFRc5aq", "subscription_details":{ "metadata":[ ] }, "subtotal":0, "subtotal_excluding_tax":0, "tax":null, "test_clock":null, "total":0, "total_discount_amounts":[ ], "total_excluding_tax":0, "total_tax_amounts":[ ], "transfer_data":null, "webhooks_delivered_at":1714332393 } }, "livemode":false, "pending_webhooks":1, "request":{ "id":"req_zLokxMgCAsYwfR", "idempotency_key":"8f55d877-a1a8-4ea2-8502-5f489c88496c" }, "type":"invoice.payment_succeeded" } } } } ```
customer.subscription.created ```json { "event":{ "Laravel\\Cashier\\Events\\WebhookReceived":{ "payload":{ "id":"evt_1PAdMrHQLg2q8v4m1SWjlqYe", "object":"event", "api_version":"2023-10-16", "created":1714332393, "data":{ "object":{ "id":"sub_1PAdMrHQLg2q8v4mZCFRc5aq", "object":"subscription", "application":null, "application_fee_percent":null, "automatic_tax":{ "enabled":false, "liability":null }, "billing_cycle_anchor":1745868392, "billing_cycle_anchor_config":null, "billing_thresholds":null, "cancel_at":null, "cancel_at_period_end":false, "canceled_at":null, "cancellation_details":{ "comment":null, "feedback":null, "reason":null }, "collection_method":"charge_automatically", "created":1714332393, "currency":"gbp", "current_period_end":1745868392, "current_period_start":1714332393, "customer":"cus_Q0egD21BpeM2h1", "days_until_due":null, "default_payment_method":null, "default_source":null, "default_tax_rates":[ ], "description":null, "discount":null, "discounts":[ ], "ended_at":null, "invoice_settings":{ "account_tax_ids":null, "issuer":{ "type":"self" } }, "items":{ "object":"list", "data":[ { "id":"si_Q0egfNnZYUWptv", "object":"subscription_item", "billing_thresholds":null, "created":1714332393, "discounts":[ ], "metadata":[ ], "plan":{ "id":"price_1NP3ytHQLg2q8v4mMJuTnKJ8", "object":"plan", "active":true, "aggregate_usage":null, "amount":799, "amount_decimal":"799", "billing_scheme":"per_unit", "created":1688218615, "currency":"gbp", "interval":"month", "interval_count":1, "livemode":false, "metadata":[ ], "meter":null, "nickname":null, "product":"prod_OBQqH7pzxYHPQM", "tiers_mode":null, "transform_usage":null, "trial_period_days":null, "usage_type":"licensed" }, "price":{ "id":"price_1NP3ytHQLg2q8v4mMJuTnKJ8", "object":"price", "active":true, "billing_scheme":"per_unit", "created":1688218615, "currency":"gbp", "custom_unit_amount":null, "livemode":false, "lookup_key":null, "metadata":[ ], "nickname":null, "product":"prod_OBQqH7pzxYHPQM", "recurring":{ "aggregate_usage":null, "interval":"month", "interval_count":1, "meter":null, "trial_period_days":null, "usage_type":"licensed" }, "tax_behavior":"unspecified", "tiers_mode":null, "transform_quantity":null, "type":"recurring", "unit_amount":799, "unit_amount_decimal":"799" }, "quantity":1, "subscription":"sub_1PAdMrHQLg2q8v4mZCFRc5aq", "tax_rates":[ ] } ], "has_more":false, "total_count":1, "url":"/v1/subscription_items?subscription=sub_1PAdMrHQLg2q8v4mZCFRc5aq" }, "latest_invoice":"in_1PAdMrHQLg2q8v4mG3KZ1Z6f", "livemode":false, "metadata":[ ], "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":"seti_1PAdMrHQLg2q8v4mAjooZgkP", "pending_update":null, "plan":{ "id":"price_1NP3ytHQLg2q8v4mMJuTnKJ8", "object":"plan", "active":true, "aggregate_usage":null, "amount":799, "amount_decimal":"799", "billing_scheme":"per_unit", "created":1688218615, "currency":"gbp", "interval":"month", "interval_count":1, "livemode":false, "metadata":[ ], "meter":null, "nickname":null, "product":"prod_OBQqH7pzxYHPQM", "tiers_mode":null, "transform_usage":null, "trial_period_days":null, "usage_type":"licensed" }, "quantity":1, "schedule":null, "start_date":1714332393, "status":"trialing", "test_clock":null, "transfer_data":null, "trial_end":1745868392, "trial_settings":{ "end_behavior":{ "missing_payment_method":"create_invoice" } }, "trial_start":1714332393 } }, "livemode":false, "pending_webhooks":1, "request":{ "id":"req_zLokxMgCAsYwfR", "idempotency_key":"8f55d877-a1a8-4ea2-8502-5f489c88496c" }, "type":"customer.subscription.created" } } } } ```
driesvints commented 3 months ago

This is because of https://github.com/laravel/cashier-stripe/pull/1628. When a subscription starts, we clear the generic trial period because it's no longer needed because your customer is now paying for their subscription.

The code you share is also an odd combination of a generic trial and a subscription trial, something we don't support and never intended to be combined. You either choose one or the other but not both.

sts-ryan-holton commented 3 months ago

Ah right. For my scenario the user is on effectively a "lifetime" subscription, which I'm utilising the trial for. They do not add payment methods, this is because they've bought a "redemption code" through a third-party vendor and are redeeming it on their account at sign-up.

How might I manage this? Because I'm setting the trial for a year, so Stripe shows their trial ending in a year, but I need to extend their trial every month or so by a month so it never ends, thus "lifetime".

Could I just call the extend method?

@driesvints

driesvints commented 3 months ago

We can't provide support for a customization like that sorry. Lifetime subscriptions can also be granted by providing a 100% discount coupon code once they did a one-time purchase for a lifetime subscription. If you want to take things further you can keep a record of one time purchases in the app and then check if they purchased a "lifetime" subscription. There's some very basic starting docs on this in Cashier's QuickStart docs: https://laravel.com/docs/11.x/billing#quickstart-selling-products