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

Duplicate entry stripe_id #1678

Closed sebastianro92 closed 2 months ago

sebastianro92 commented 2 months ago

Cashier Stripe Version

15.0

Laravel Version

10

PHP Version

8.3.6

Database Driver & Version

MYSQL, 8.0.36

Description

I use Stripe Webhooks, but when i cancelling a customer subscription and create a new subscription, received then next error:

[2024-05-27 00:05:04] local.ERROR: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'sub_1PKunxFd5Q4HlP154MBNJXW5' for key 'subscriptions.subscriptions_stripe_id_unique' (Connection: mysql, SQL: insert into subscriptions (type, stripe_id, stripe_status, stripe_price, quantity, trial_ends_at, ends_at, user_id, updated_at, created_at) values (default, sub_1PKunxFd5Q4HlP154MBNJXW5, incomplete, price_1Lu0DCFd5Q4HlP15qpPSBDWb, 1, ?, ?, 1, 2024-05-27 00:05:04, 2024-05-27 00:05:04)) {"exception":"[object] (Illuminate\Database\UniqueConstraintViolationException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'sub_1PKunxFd5Q4HlP154MBNJXW5' for key 'subscriptions.subscriptions_stripe_id_unique' (Connection: mysql, SQL: insert into subscriptions (type, stripe_id, stripe_status, stripe_price, quantity, trial_ends_at, ends_at, user_id, updated_at, created_at) values (default, sub_1PKunxFd5Q4HlP154MBNJXW5, incomplete, price_1Lu0DCFd5Q4HlP15qpPSBDWb, 1, ?, ?, 1, 2024-05-27 00:05:04, 2024-05-27 00:05:04)) at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:824) [stacktrace]

0 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(783): Illuminate\Database\Connection->runQueryCallback()

1 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(576): Illuminate\Database\Connection->run()

2 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(540): Illuminate\Database\Connection->statement()

3 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Query/Processors/Processor.php(32): Illuminate\Database\Connection->insert()

4 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(3507): Illuminate\Database\Query\Processors\Processor->processInsertGetId()

5 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php(1982): Illuminate\Database\Query\Builder->insertGetId()

6 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(1333): Illuminate\Database\Eloquent\Builder->__call()

7 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(1298): Illuminate\Database\Eloquent\Model->insertAndSetId()

8 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(1137): Illuminate\Database\Eloquent\Model->performInsert()

9 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php(342): Illuminate\Database\Eloquent\Model->save()

10 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/helpers.php(307): Illuminate\Database\Eloquent\Relations\HasOneOrMany->Illuminate\Database\Eloquent\Relations\{closure}()

11 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php(339): tap()

12 /var/www/html/vendor/laravel/cashier/src/Http/Controllers/WebhookController.php(83): Illuminate\Database\Eloquent\Relations\HasOneOrMany->create()

13 /var/www/html/vendor/laravel/cashier/src/Http/Controllers/WebhookController.php(50): Laravel\Cashier\Http\Controllers\WebhookController->handleCustomerSubscriptionCreated()

14 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): Laravel\Cashier\Http\Controllers\WebhookController->handleWebhook()

15 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(43): Illuminate\Routing\Controller->callAction()

16 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(259): Illuminate\Routing\ControllerDispatcher->dispatch()

17 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(205): Illuminate\Routing\Route->runController()

18 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(806): Illuminate\Routing\Route->run()

19 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\Routing\Router->Illuminate\Routing\{closure}()

20 /var/www/html/vendor/laravel/cashier/src/Http/Middleware/VerifyWebhookSignature.php(34): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

21 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Laravel\Cashier\Http\Middleware\VerifyWebhookSignature->handle()

22 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

23 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(805): Illuminate\Pipeline\Pipeline->then()

24 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(784): Illuminate\Routing\Router->runRouteWithinStack()

25 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(748): Illuminate\Routing\Router->runRoute()

26 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(737): Illuminate\Routing\Router->dispatchToRoute()

27 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\Routing\Router->dispatch()

28 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\Foundation\Http\Kernel->Illuminate\Foundation\Http\{closure}()

29 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

30 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(31): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()

31 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull->handle()

32 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

33 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(40): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()

34 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Foundation\Http\Middleware\TrimStrings->handle()

35 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

36 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Foundation\Http\Middleware\ValidatePostSize->handle()

37 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(99): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

38 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance->handle()

39 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(49): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

40 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Http\Middleware\HandleCors->handle()

41 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(39): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

42 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Http\Middleware\TrustProxies->handle()

43 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

44 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\Pipeline\Pipeline->then()

45 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter()

46 /var/www/html/vendor/laravel/octane/src/ApplicationGateway.php(36): Illuminate\Foundation\Http\Kernel->handle()

47 /var/www/html/vendor/laravel/octane/src/Worker.php(84): Laravel\Octane\ApplicationGateway->handle()

48 /var/www/html/vendor/laravel/octane/bin/roadrunner-worker(54): Laravel\Octane\Worker->handle()

49 /var/www/html/vendor/bin/roadrunner-worker(119): include('...')

50 {main}

Steps To Reproduce

Cancel a customer subscription in Stripe Dashboard, then create a new customer subscription using the Stripe checkout portal.

sebastianro92 commented 2 months ago

Its recomendable add a delay in the listener to comprobe if the "customer.subscription.created" its ok?

image
driesvints commented 2 months ago

I don't really understand this. There should only be a single customer.subscription.created event fired for each subscription. We already check for the stripe_id here: https://github.com/laravel/cashier-stripe/blob/15.x/src/Http/Controllers/WebhookController.php#L73

I assume you "retried" the webhook? But that still doesn't explains the error because there's enough time between the webhooks for handling this event without it being flaky.

Could you please provide a repo and then post exact steps to reproduce this one?

laravel new bug-report --github="--public"
sebastianro92 commented 2 months ago

@driesvints Unfortunately, there is no exact way to replicate this issue as it does not always occur, but rather occurs randomly.

However, I'm almost sure that at some point the subscription that is created for the client is repeated twice; otherwise why would you get this error?

"Duplicate entry 'sub_1PL6sUFd5Q4HlP151A2WhXwH' for key 'subscriptions.subscriptions_stripe_id_unique'"

However, although I get this error, only one record is saved in the DB and this is fine, but there is a problem, when trying to save the second record and it generates the error, the "client.subscription.created" hook not is executed, causing my Application to fail because it does not detect the event and does not execute the functions that should be executed.

I hope it helps, but I share the error log that I just obtained a few minutes ago doing the process I mention.

  1. Cancel a customer subscription from the Stripe dashboard
  2. Then using the API to create a checkout session and add a new subscription for this customer
         $checkout_session = $stripeClient->checkout->sessions->create([
             'success_url' => config('access.STRIPE_REDIRECT_BACK'),
             'cancel_url' => config('access.STRIPE_REDIRECT_BACK'),
             'line_items' => [
                 [
                     'price' => config('access.LICENCE_PRICE_KEY'),
                     'quantity' => 1,
                 ],
             ],
             'mode' => 'subscription',
             'allow_promotion_codes' => true,
             'customer' => $customer->stripe_id,
         ]);
  3. Finally receive the Hooks, this is when possibly receive an error in "customer.subscription.created" -> "Integrity constraint violation: 1062 Duplicate entry 'sub_1PL6sUFd5Q4HlP151A2WhXwH' for key 'subscriptions.subscriptions_stripe_id_unique'" and the rest fails.

laravel.log

This is my handle listener

image

Is normal that when I create a new subscription for a customer, the "customer.subscription.updated" hook is executed?

This is a console.log from my Front dev.

image

And this is the total events when i created a new customer subscription

image
driesvints commented 2 months ago

I'm sorry but I couldn't reproduce it using your example. You also seem to be working outside the context of Cashier with directly talking to the Stripe client. That's not supported I'm afraid. If you can post a clear code example that reproduces the issue I can have another look but right now I don't believe there's a major issue and this is most likely something specifically happening for you. I suggest to never work outside the context of Cashier.

And yes it's normal that updated events are sent together with the created events. Stripe updates the subscription after creating it.

sebastianro92 commented 2 months ago

Hi @driesvints maybe i have the same problem #1300 and #1307 ?

In my case is when add a new customer subscription and the error is randomly, i updated the checkout portal using cashier according your suggestions.

` $customer = auth()->user();

    $checkout_session = $customer->newSubscription('default', 'my_price_key')
        ->allowPromotionCodes()
        ->collectTaxIds()
        ->checkout([
            'success_url' => config('access.STRIPE_REDIRECT_BACK'),
            'cancel_url' => config('access.STRIPE_REDIRECT_BACK'),
        ]);

    // * Redirect client to Stripe Checkout
    return response()->json($checkout_session->url);

`

driesvints commented 2 months ago

I'm sorry @sebastianro92 but I'm going to leave things be. I just don't feel there's a widespread issue right now.