laravel / cashier-paddle

Cashier Paddle provides an expressive, fluent interface to Paddle's subscription billing services.
https://laravel.com/docs/cashier-paddle
MIT License
245 stars 57 forks source link

Incrementing quantity in subscription fails because transaction amount is too small #278

Closed korridor closed 1 month ago

korridor commented 2 months ago

Cashier Paddle Version

2.4.3

Laravel Version

11.10.0

PHP Version

8.3.7

Database Driver & Version

PostgreSQL 15

Description

I'm using Cashier Paddle with Laravel Spark, and we have prorate activated. A customer of our tried to increase their seats at the end of the billing period and we got the following error. I think the problem is that Cashier or Spark forces paddle to invoice immediately even though this is not possible, since the amount is too small. As far as I understand this, this could happen with any seat price as long as the customer increases the seats shortly before the end of the billing period.

I'm not sure if this is a bug of Laravel Spark or Cashier Paddle. Laravel Spark could probably use incrementQuantity instead of incrementAndInvoice, but that would change the behavior in all cases. I think the better solution would be for Cashier to check if the amount is too small and if the amount is too small, change the proration type from prorated_immediately to prorated_next_billing_period. Cashier could also catch exactly this error and if that happens, try it again with a different proration type, since calculating the transaction amount is done by Paddle.

Stacktrace

Laravel\Paddle\Exceptions\PaddleException: Paddle API error 'Unable to charge for Subscription update: Transaction balance is less than what we can charge. Transaction balance: 38, Minimum payment amount: 70, Currency code: EUR' occurred
#92 /extensions/Billing/vendor/laravel/cashier-paddle/src/Cashier.php(137): Laravel\Paddle\Cashier::api
#91 /extensions/Billing/vendor/laravel/cashier-paddle/src/Subscription.php(822): Laravel\Paddle\Subscription::updatePaddleSubscription
#90 /extensions/Billing/vendor/laravel/cashier-paddle/src/Subscription.php(551): Laravel\Paddle\Subscription::updateQuantity
#89 /extensions/Billing/vendor/laravel/cashier-paddle/src/Subscription.php(503): Laravel\Paddle\Subscription::incrementAndInvoice
#88 /extensions/Billing/vendor/laravel/spark-paddle/src/Billable.php(102): Extensions\Billing\App\Models\Organization::addSeat
#87 /extensions/Billing/app/Listeners/AddSeatToOrganization.php(28): Extensions\Billing\App\Listeners\AddSeatToOrganization::handle
#86 /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php(478): Illuminate\Events\Dispatcher::Illuminate\Events\{closure}
#85 /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php(286): Illuminate\Events\Dispatcher::invokeListeners
#84 /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php(266): Illuminate\Events\Dispatcher::dispatch
#83 /vendor/laravel/framework/src/Illuminate/Foundation/helpers.php(453): event
#82 /vendor/laravel/framework/src/Illuminate/Foundation/Events/Dispatchable.php(14): Laravel\Jetstream\Events\AddingTeamMember::dispatch
#81 /app/Actions/Jetstream/AddOrganizationMember.php(39): App\Actions\Jetstream\AddOrganizationMember::add
#80 /vendor/laravel/jetstream/src/Http/Controllers/TeamInvitationController.php(27): Laravel\Jetstream\Http\Controllers\TeamInvitationController::accept
#79 /vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): Illuminate\Routing\Controller::callAction
#78 /vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(43): Illuminate\Routing\ControllerDispatcher::dispatch
#77 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php(21): Sentry\Laravel\Tracing\Routing\TracingControllerDispatcherTracing::Sentry\Laravel\Tracing\Routing\{closure}
#76 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.php(17): Sentry\Laravel\Tracing\Routing\TracingRoutingDispatcher::wrapRouteDispatch
#75 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php(20): Sentry\Laravel\Tracing\Routing\TracingControllerDispatcherTracing::dispatch
#74 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(260): Illuminate\Routing\Route::runController
#73 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(206): Illuminate\Routing\Route::run
#72 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(806): Illuminate\Routing\Router::Illuminate\Routing\{closure}
#71 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#70 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/ValidateSignature.php(70): Illuminate\Routing\Middleware\ValidateSignature::handle
#69 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#68 /app/Http/Middleware/EnsureEmailIsVerified.php(31): App\Http\Middleware\EnsureEmailIsVerified::handle
#67 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#66 /vendor/laravel/passport/src/Http/Middleware/CreateFreshApiToken.php(63): Laravel\Passport\Http\Middleware\CreateFreshApiToken::handle
#65 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#64 /vendor/laravel/framework/src/Illuminate/Http/Middleware/AddLinkHeadersForPreloadedAssets.php(19): Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::handle
#63 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#62 /app/Http/Middleware/ShareInertiaData.php(106): App\Http\Middleware\ShareInertiaData::handle
#61 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#60 /vendor/inertiajs/inertia-laravel/src/Middleware.php(86): Inertia\Middleware::handle
#59 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#58 /vendor/laravel/jetstream/src/Http/Middleware/ShareInertiaData.php(69): Laravel\Jetstream\Http\Middleware\ShareInertiaData::handle
#57 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#56 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(50): Illuminate\Routing\Middleware\SubstituteBindings::handle
#55 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#54 /vendor/laravel/framework/src/Illuminate/Session/Middleware/AuthenticateSession.php(67): Illuminate\Session\Middleware\AuthenticateSession::handle
#53 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#52 /vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php(64): Illuminate\Auth\Middleware\Authenticate::handle
#51 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#50 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php(88): Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::handle
#49 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#48 /vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php(49): Illuminate\View\Middleware\ShareErrorsFromSession::handle
#47 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#46 /vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(121): Illuminate\Session\Middleware\StartSession::handleStatefulRequest
#45 /vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(64): Illuminate\Session\Middleware\StartSession::handle
#44 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#43 /vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php(37): Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::handle
#42 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#41 /vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php(75): Illuminate\Cookie\Middleware\EncryptCookies::handle
#40 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#39 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\Pipeline\Pipeline::then
#38 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(805): Illuminate\Routing\Router::runRouteWithinStack
#37 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(784): Illuminate\Routing\Router::runRoute
#36 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(748): Illuminate\Routing\Router::dispatchToRoute
#35 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(737): Illuminate\Routing\Router::dispatch
#34 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\Foundation\Http\Kernel::Illuminate\Foundation\Http\{closure}
#33 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#32 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Http/FlushEventsMiddleware.php(13): Sentry\Laravel\Http\FlushEventsMiddleware::handle
#31 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#30 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Http/SetRequestIpMiddleware.php(45): Sentry\Laravel\Http\SetRequestIpMiddleware::handle
#29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#28 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Http/SetRequestMiddleware.php(31): Sentry\Laravel\Http\SetRequestMiddleware::handle
#27 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#26 /vendor/livewire/livewire/src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php(19): Livewire\Features\SupportDisablingBackButtonCache\DisableBackButtonCacheMiddleware::handle
#25 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#24 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Foundation\Http\Middleware\TransformsRequest::handle
#23 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(31): Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::handle
#22 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#21 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Foundation\Http\Middleware\TransformsRequest::handle
#20 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\Foundation\Http\Middleware\TrimStrings::handle
#19 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#18 /vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php(27): Illuminate\Http\Middleware\ValidatePostSize::handle
#17 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#16 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(110): Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::handle
#15 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#14 /vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(49): Illuminate\Http\Middleware\HandleCors::handle
#13 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#12 /vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(57): Illuminate\Http\Middleware\TrustProxies::handle
#11 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#10 /extensions/Services/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Tracing/Middleware.php(97): Sentry\Laravel\Tracing\Middleware::handle
#9 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#8 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\Pipeline\Pipeline::then
#7 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter
#6 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\Foundation\Http\Kernel::handle
#5 /vendor/laravel/octane/src/ApplicationGateway.php(36): Laravel\Octane\ApplicationGateway::handle
#4 /vendor/laravel/octane/src/Worker.php(84): Laravel\Octane\Worker::handle
#3 /vendor/laravel/octane/bin/frankenphp-worker.php(53): {closure}
#2 [internal](0): frankenphp_handle_request
#1 /vendor/laravel/octane/bin/frankenphp-worker.php(74): require
#0 /public/frankenphp-worker.php(3): null

Steps To Reproduce

  1. Create a subscription with a product that is not expensive (f.e. 10 dollars)
  2. Wait until shortly before the end of the billing period (f.e. for a monthly subscription, a day before the customer is charged for the next month)
  3. Increase the quantity of the products in the subscription.
driesvints commented 2 months ago

Hi Korridor. Could you provide me with some code to reproduce this? I realise you're saying this is happening in Spark but some code to reproduce this would be wonderful. Also the full body of the request that's going to Paddle if possible as I could inspect that against the API.

crynobone commented 1 month ago

Hey there,

We're closing this issue because it's inactive, already solved, old, or not relevant anymore. Feel free to open up a new issue if you're still experiencing this problem.