├── LICENSE.md ├── README.md ├── composer.json ├── config └── cashier.php ├── database ├── factories │ ├── SubscriptionFactory.php │ └── SubscriptionItemFactory.php └── migrations │ ├── 2019_05_03_000001_create_customer_columns.php │ ├── 2019_05_03_000002_create_subscriptions_table.php │ └── 2019_05_03_000003_create_subscription_items_table.php ├── resources └── views │ ├── invoice.blade.php │ └── payment.blade.php ├── routes └── web.php ├── src ├── Billable.php ├── Cashier.php ├── CashierServiceProvider.php ├── Checkout.php ├── CheckoutBuilder.php ├── Concerns │ ├── AllowsCoupons.php │ ├── HandlesPaymentFailures.php │ ├── HandlesTaxes.php │ ├── InteractsWithPaymentBehavior.php │ ├── ManagesCustomer.php │ ├── ManagesInvoices.php │ ├── ManagesPaymentMethods.php │ ├── ManagesSubscriptions.php │ ├── ManagesUsageBilling.php │ ├── PerformsCharges.php │ └── Prorates.php ├── Console │ └── WebhookCommand.php ├── Contracts │ └── InvoiceRenderer.php ├── Coupon.php ├── CustomerBalanceTransaction.php ├── Discount.php ├── Events │ ├── WebhookHandled.php │ └── WebhookReceived.php ├── Exceptions │ ├── CustomerAlreadyCreated.php │ ├── IncompletePayment.php │ ├── InvalidCustomer.php │ ├── InvalidCustomerBalanceTransaction.php │ ├── InvalidInvoice.php │ ├── InvalidPaymentMethod.php │ └── SubscriptionUpdateFailure.php ├── Http │ ├── Controllers │ │ ├── PaymentController.php │ │ └── WebhookController.php │ └── Middleware │ │ ├── VerifyRedirectUrl.php │ │ └── VerifyWebhookSignature.php ├── Invoice.php ├── InvoiceLineItem.php ├── Invoices │ └── DompdfInvoiceRenderer.php ├── Jobs │ └── SyncCustomerDetails.php ├── Logger.php ├── Notifications │ └── ConfirmPayment.php ├── Payment.php ├── PaymentMethod.php ├── PromotionCode.php ├── Subscription.php ├── SubscriptionBuilder.php ├── SubscriptionItem.php └── Tax.php └── testbench.yaml /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo Laravel Cashier Stripe

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## Introduction 11 | 12 | Laravel Cashier provides an expressive, fluent interface to [Stripe's](https://stripe.com) subscription billing services. It handles almost all of the boilerplate subscription billing code you are dreading writing. In addition to basic subscription management, Cashier can handle coupons, swapping subscription, subscription "quantities", cancellation grace periods, and even generate invoice PDFs. 13 | 14 | ## Official Documentation 15 | 16 | Documentation for Cashier can be found on the [Laravel website](https://laravel.com/docs/billing). 17 | 18 | ## Contributing 19 | 20 | Thank you for considering contributing to Cashier! You can read the contribution guide [here](.github/CONTRIBUTING.md). 21 | 22 | ## Code of Conduct 23 | 24 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 25 | 26 | ## Security Vulnerabilities 27 | 28 | Please review [our security policy](https://github.com/laravel/cashier/security/policy) on how to report security vulnerabilities. 29 | 30 | ## License 31 | 32 | Laravel Cashier is open-sourced software licensed under the [MIT license](LICENSE.md). 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/cashier", 3 | "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", 4 | "keywords": ["laravel", "stripe", "billing"], 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/laravel/cashier/issues", 8 | "source": "https://github.com/laravel/cashier" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | }, 15 | { 16 | "name": "Dries Vints", 17 | "email": "dries@laravel.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1", 22 | "ext-json": "*", 23 | "illuminate/console": "^10.0|^11.0|^12.0", 24 | "illuminate/contracts": "^10.0|^11.0|^12.0", 25 | "illuminate/database": "^10.0|^11.0|^12.0", 26 | "illuminate/http": "^10.0|^11.0|^12.0", 27 | "illuminate/log": "^10.0|^11.0|^12.0", 28 | "illuminate/notifications": "^10.0|^11.0|^12.0", 29 | "illuminate/pagination": "^10.0|^11.0|^12.0", 30 | "illuminate/routing": "^10.0|^11.0|^12.0", 31 | "illuminate/support": "^10.0|^11.0|^12.0", 32 | "illuminate/view": "^10.0|^11.0|^12.0", 33 | "moneyphp/money": "^4.0", 34 | "nesbot/carbon": "^2.0|^3.0", 35 | "stripe/stripe-php": "^16.2", 36 | "symfony/console": "^6.0|^7.0", 37 | "symfony/http-kernel": "^6.0|^7.0", 38 | "symfony/polyfill-intl-icu": "^1.22.1" 39 | }, 40 | "require-dev": { 41 | "dompdf/dompdf": "^2.0", 42 | "mockery/mockery": "^1.0", 43 | "orchestra/testbench": "^8.18|^9.0|^10.0", 44 | "phpstan/phpstan": "^1.10", 45 | "phpunit/phpunit": "^10.4|^11.5" 46 | }, 47 | "suggest": { 48 | "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values.", 49 | "dompdf/dompdf": "Required when generating and downloading invoice PDF's using Dompdf (^1.0.1|^2.0)." 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "Laravel\\Cashier\\": "src/", 54 | "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" 55 | } 56 | }, 57 | "autoload-dev": { 58 | "psr-4": { 59 | "Laravel\\Cashier\\Tests\\": "tests/" 60 | } 61 | }, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-master": "15.x-dev" 65 | }, 66 | "laravel": { 67 | "providers": [ 68 | "Laravel\\Cashier\\CashierServiceProvider" 69 | ] 70 | } 71 | }, 72 | "config": { 73 | "sort-packages": true 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable": true 77 | } 78 | -------------------------------------------------------------------------------- /config/cashier.php: -------------------------------------------------------------------------------- 1 | env('STRIPE_KEY'), 20 | 21 | 'secret' => env('STRIPE_SECRET'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Cashier Path 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This is the base URI path where Cashier's views, such as the payment 29 | | verification screen, will be available from. You're free to tweak 30 | | this path according to your preferences and application design. 31 | | 32 | */ 33 | 34 | 'path' => env('CASHIER_PATH', 'stripe'), 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Stripe Webhooks 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Your Stripe webhook secret is used to prevent unauthorized requests to 42 | | your Stripe webhook handling controllers. The tolerance setting will 43 | | check the drift between the current time and the signed request's. 44 | | 45 | */ 46 | 47 | 'webhook' => [ 48 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 49 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 50 | 'events' => WebhookCommand::DEFAULT_EVENTS, 51 | ], 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Currency 56 | |-------------------------------------------------------------------------- 57 | | 58 | | This is the default currency that will be used when generating charges 59 | | from your application. Of course, you are welcome to use any of the 60 | | various world currencies that are currently supported via Stripe. 61 | | 62 | */ 63 | 64 | 'currency' => env('CASHIER_CURRENCY', 'usd'), 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Currency Locale 69 | |-------------------------------------------------------------------------- 70 | | 71 | | This is the default locale in which your money values are formatted in 72 | | for display. To utilize other locales besides the default en locale 73 | | verify you have the "intl" PHP extension installed on the system. 74 | | 75 | */ 76 | 77 | 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Payment Confirmation Notification 82 | |-------------------------------------------------------------------------- 83 | | 84 | | If this setting is enabled, Cashier will automatically notify customers 85 | | whose payments require additional verification. You should listen to 86 | | Stripe's webhooks in order for this feature to function correctly. 87 | | 88 | */ 89 | 90 | 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Invoice Settings 95 | |-------------------------------------------------------------------------- 96 | | 97 | | The following options determine how Cashier invoices are converted from 98 | | HTML into PDFs. You're free to change the options based on the needs 99 | | of your application or your preferences regarding invoice styling. 100 | | 101 | */ 102 | 103 | 'invoices' => [ 104 | 'renderer' => env('CASHIER_INVOICE_RENDERER', DompdfInvoiceRenderer::class), 105 | 106 | 'options' => [ 107 | // Supported: 'letter', 'legal', 'A4' 108 | 'paper' => env('CASHIER_PAPER', 'letter'), 109 | 110 | 'remote_enabled' => env('CASHIER_REMOTE_ENABLED', false), 111 | ], 112 | ], 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Stripe Logger 117 | |-------------------------------------------------------------------------- 118 | | 119 | | This setting defines which logging channel will be used by the Stripe 120 | | library to write log messages. You are free to specify any of your 121 | | logging channels listed inside the "logging" configuration file. 122 | | 123 | */ 124 | 125 | 'logger' => env('CASHIER_LOGGER'), 126 | 127 | ]; 128 | -------------------------------------------------------------------------------- /database/factories/SubscriptionFactory.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function definition(): array 28 | { 29 | $model = Cashier::$customerModel; 30 | 31 | return [ 32 | (new $model)->getForeignKey() => ($model)::factory(), 33 | 'type' => 'default', 34 | 'stripe_id' => 'sub_'.Str::random(40), 35 | 'stripe_status' => StripeSubscription::STATUS_ACTIVE, 36 | 'stripe_price' => null, 37 | 'quantity' => null, 38 | 'trial_ends_at' => null, 39 | 'ends_at' => null, 40 | ]; 41 | } 42 | 43 | /** 44 | * Add a price identifier to the model. 45 | * 46 | * @return $this 47 | */ 48 | public function withPrice(StripePrice|string $price): static 49 | { 50 | return $this->state([ 51 | 'stripe_price' => $price instanceof StripePrice ? $price->id : $price, 52 | ]); 53 | } 54 | 55 | /** 56 | * Mark the subscription as active. 57 | * 58 | * @return $this 59 | */ 60 | public function active(): static 61 | { 62 | return $this->state([ 63 | 'stripe_status' => StripeSubscription::STATUS_ACTIVE, 64 | ]); 65 | } 66 | 67 | /** 68 | * Mark the subscription as being within a trial period. 69 | * 70 | * @return $this 71 | */ 72 | public function trialing(?DateTimeInterface $trialEndsAt = null): static 73 | { 74 | return $this->state([ 75 | 'stripe_status' => StripeSubscription::STATUS_TRIALING, 76 | 'trial_ends_at' => $trialEndsAt, 77 | ]); 78 | } 79 | 80 | /** 81 | * Mark the subscription as canceled. 82 | * 83 | * @return $this 84 | */ 85 | public function canceled(): static 86 | { 87 | return $this->state([ 88 | 'stripe_status' => StripeSubscription::STATUS_CANCELED, 89 | 'ends_at' => now(), 90 | ]); 91 | } 92 | 93 | /** 94 | * Mark the subscription as incomplete. 95 | * 96 | * @return $this 97 | */ 98 | public function incomplete(): static 99 | { 100 | return $this->state([ 101 | 'stripe_status' => StripeSubscription::STATUS_INCOMPLETE, 102 | ]); 103 | } 104 | 105 | /** 106 | * Mark the subscription as incomplete where the allowed completion period has expired. 107 | * 108 | * @return $this 109 | */ 110 | public function incompleteAndExpired(): static 111 | { 112 | return $this->state([ 113 | 'stripe_status' => StripeSubscription::STATUS_INCOMPLETE_EXPIRED, 114 | ]); 115 | } 116 | 117 | /** 118 | * Mark the subscription as being past the due date. 119 | * 120 | * @return $this 121 | */ 122 | public function pastDue(): static 123 | { 124 | return $this->state([ 125 | 'stripe_status' => StripeSubscription::STATUS_PAST_DUE, 126 | ]); 127 | } 128 | 129 | /** 130 | * Mark the subscription as unpaid. 131 | * 132 | * @return $this 133 | */ 134 | public function unpaid(): static 135 | { 136 | return $this->state([ 137 | 'stripe_status' => StripeSubscription::STATUS_UNPAID, 138 | ]); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /database/factories/SubscriptionItemFactory.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'subscription_id' => Subscription::factory(), 28 | 'stripe_id' => 'si_'.Str::random(40), 29 | 'stripe_product' => 'prod_'.Str::random(40), 30 | 'stripe_price' => 'price_'.Str::random(40), 31 | 'quantity' => null, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000001_create_customer_columns.php: -------------------------------------------------------------------------------- 1 | string('stripe_id')->nullable()->index(); 16 | $table->string('pm_type')->nullable(); 17 | $table->string('pm_last_four', 4)->nullable(); 18 | $table->timestamp('trial_ends_at')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('users', function (Blueprint $table) { 28 | $table->dropIndex([ 29 | 'stripe_id', 30 | ]); 31 | 32 | $table->dropColumn([ 33 | 'stripe_id', 34 | 'pm_type', 35 | 'pm_last_four', 36 | 'trial_ends_at', 37 | ]); 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000002_create_subscriptions_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id'); 17 | $table->string('type'); 18 | $table->string('stripe_id')->unique(); 19 | $table->string('stripe_status'); 20 | $table->string('stripe_price')->nullable(); 21 | $table->integer('quantity')->nullable(); 22 | $table->timestamp('trial_ends_at')->nullable(); 23 | $table->timestamp('ends_at')->nullable(); 24 | $table->timestamps(); 25 | 26 | $table->index(['user_id', 'stripe_status']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('subscriptions'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000003_create_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('subscription_id'); 17 | $table->string('stripe_id')->unique(); 18 | $table->string('stripe_product'); 19 | $table->string('stripe_price'); 20 | $table->integer('quantity')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->index(['subscription_id', 'stripe_price']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('subscription_items'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /resources/views/invoice.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Invoice 8 | 9 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 78 | 79 | 80 | 85 | 86 | 87 | 123 | 163 | 164 | 165 | 180 | 181 | 182 | 322 | 323 |
53 | 54 | Invoice 55 | 56 | @if ($invoice->paid) 57 | (Paid) 58 | @endif 59 | 60 | 61 | 62 |

63 | @isset ($product) 64 | Product: {{ $product }}
65 | @endisset 66 | 67 | Date: {{ $invoice->date()->toFormattedDateString() }}
68 | 69 | @if ($dueDate = $invoice->dueDate()) 70 | Due date: {{ $dueDate->toFormattedDateString() }}
71 | @endif 72 | 73 | @if ($invoiceId = $id ?? $invoice->number) 74 | Invoice Number: {{ $invoiceId }}
75 | @endif 76 |

77 |
81 | 82 | {{ $header ?? $vendor ?? $invoice->account_name }} 83 | 84 |
88 | 89 | {{ $vendor ?? $invoice->account_name }}
90 | 91 | @isset($street) 92 | {{ $street }}
93 | @endisset 94 | 95 | @isset($location) 96 | {{ $location }}
97 | @endisset 98 | 99 | @isset($country) 100 | {{ $country }}
101 | @endisset 102 | 103 | @isset($phone) 104 | {{ $phone }}
105 | @endisset 106 | 107 | @isset($email) 108 | {{ $email }}
109 | @endisset 110 | 111 | @isset($url) 112 | {{ $url }}
113 | @endisset 114 | 115 | @isset($vendorVat) 116 | {{ $vendorVat }}
117 | @else 118 | @foreach ($invoice->accountTaxIds() as $taxId) 119 | {{ $taxId->value }}
120 | @endforeach 121 | @endisset 122 |
124 | 125 | Recipient
126 | 127 | {{ $invoice->customer_name ?? $invoice->customer_email }}
128 | 129 | @if ($address = $invoice->customer_address) 130 | @if ($address->line1) 131 | {{ $address->line1 }}
132 | @endif 133 | 134 | @if ($address->line2) 135 | {{ $address->line2 }}
136 | @endif 137 | 138 | @if ($address->city) 139 | {{ $address->city }}
140 | @endif 141 | 142 | @if ($address->state || $address->postal_code) 143 | {{ implode(' ', [$address->state, $address->postal_code]) }}
144 | @endif 145 | 146 | @if ($address->country) 147 | {{ $address->country }}
148 | @endif 149 | @endif 150 | 151 | @if ($invoice->customer_phone) 152 | {{ $invoice->customer_phone }}
153 | @endif 154 | 155 | @if ($invoice->customer_name) 156 | {{ $invoice->customer_email }}
157 | @endif 158 | 159 | @foreach ($invoice->customerTaxIds() as $taxId) 160 | {{ $taxId->value }}
161 | @endforeach 162 |
166 | 167 | @if ($invoice->description) 168 |

169 | {{ $invoice->description }} 170 |

171 | @endif 172 | 173 | 174 | @if (isset($vat)) 175 |

176 | {{ $vat }} 177 |

178 | @endif 179 |
183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | @if ($invoice->hasTax()) 191 | 192 | @endif 193 | 194 | 195 | 196 | 197 | 198 | @foreach ($invoice->invoiceLineItems() as $item) 199 | 200 | 209 | 210 | 211 | 212 | 213 | @if ($invoice->hasTax()) 214 | 227 | @endif 228 | 229 | 230 | 231 | @endforeach 232 | 233 | 234 | @if ($invoice->hasDiscount() || $invoice->hasTax() || $invoice->hasStartingBalance()) 235 | 236 | 237 | 238 | 239 | 240 | @endif 241 | 242 | 243 | @if ($invoice->hasDiscount()) 244 | @foreach ($invoice->discounts() as $discount) 245 | @php($coupon = $discount->coupon()) 246 | 247 | 248 | 249 | 256 | 257 | 258 | 259 | @endforeach 260 | @endif 261 | 262 | 263 | @unless ($invoice->isNotTaxExempt()) 264 | 265 | 266 | 273 | 274 | 275 | @else 276 | @foreach ($invoice->taxes() as $tax) 277 | 278 | 279 | 283 | 284 | 285 | @endforeach 286 | @endunless 287 | 288 | 289 | 290 | 291 | 294 | 297 | 298 | 299 | 300 | @if ($invoice->hasAppliedBalance()) 301 | 302 | 303 | 306 | 307 | 308 | @endif 309 | 310 | 311 | 312 | 313 | 316 | 319 | 320 |
DescriptionQtyUnit priceTaxAmount
201 | {{ $item->description }} 202 | 203 | @if ($item->hasPeriod() && ! $item->periodStartAndEndAreEqual()) 204 |
205 | {{ $item->startDate() }} - {{ $item->endDate() }} 206 | 207 | @endif 208 |
{{ $item->quantity }}{{ $item->unitAmountExcludingTax() }} 215 | @if ($inclusiveTaxPercentage = $item->inclusiveTaxPercentage()) 216 | {{ $inclusiveTaxPercentage }}% incl. 217 | @endif 218 | 219 | @if ($item->hasBothInclusiveAndExclusiveTax()) 220 | + 221 | @endif 222 | 223 | @if ($exclusiveTaxPercentage = $item->exclusiveTaxPercentage()) 224 | {{ $exclusiveTaxPercentage }}% 225 | @endif 226 | {{ $item->total() }}
Subtotal{{ $invoice->subtotal() }}
250 | @if ($coupon->isPercentage()) 251 | {{ $coupon->name() }} ({{ $coupon->percentOff() }}% Off) 252 | @else 253 | {{ $coupon->name() }} ({{ $coupon->amountOff() }} Off) 254 | @endif 255 | -{{ $invoice->discountFor($discount) }}
267 | @if ($invoice->isTaxExempt()) 268 | Tax is exempted 269 | @else 270 | Tax to be paid on reverse charge basis 271 | @endif 272 |
280 | {{ $tax->display_name }} {{ $tax->jurisdiction ? ' - '.$tax->jurisdiction : '' }} 281 | ({{ $tax->percentage }}%{{ $tax->isInclusive() ? ' incl.' : '' }}) 282 | {{ $tax->amount() }}
292 | Total 293 | 295 | {{ $invoice->realTotal() }} 296 |
304 | Applied balance 305 | {{ $invoice->appliedBalance() }}
314 | Amount due 315 | 317 | {{ $invoice->amountDue() }} 318 |
321 |
324 |
325 | 326 | 327 | 328 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('payment'); 6 | Route::post('webhook', 'WebhookController@handleWebhook')->name('webhook'); 7 | -------------------------------------------------------------------------------- /src/Billable.php: -------------------------------------------------------------------------------- 1 | id : $stripeId; 103 | 104 | $model = static::$customerModel; 105 | 106 | $builder = in_array(SoftDeletes::class, class_uses_recursive($model)) 107 | ? $model::withTrashed() 108 | : new $model; 109 | 110 | return $stripeId ? $builder->where('stripe_id', $stripeId)->first() : null; 111 | } 112 | 113 | /** 114 | * Get the Stripe SDK client. 115 | * 116 | * @param array $options 117 | * @return \Stripe\StripeClient 118 | */ 119 | public static function stripe(array $options = []) 120 | { 121 | $config = array_merge([ 122 | 'api_key' => $options['api_key'] ?? config('cashier.secret'), 123 | 'stripe_version' => static::STRIPE_VERSION, 124 | 'api_base' => static::$apiBaseUrl, 125 | ], $options); 126 | 127 | return app(StripeClient::class, ['config' => $config]); 128 | } 129 | 130 | /** 131 | * Set the custom currency formatter. 132 | * 133 | * @param callable $callback 134 | * @return void 135 | */ 136 | public static function formatCurrencyUsing(callable $callback) 137 | { 138 | static::$formatCurrencyUsing = $callback; 139 | } 140 | 141 | /** 142 | * Format the given amount into a displayable currency. 143 | * 144 | * @param int $amount 145 | * @param string|null $currency 146 | * @param string|null $locale 147 | * @param array $options 148 | * @return string 149 | */ 150 | public static function formatAmount($amount, $currency = null, $locale = null, array $options = []) 151 | { 152 | if (static::$formatCurrencyUsing) { 153 | return call_user_func(static::$formatCurrencyUsing, $amount, $currency, $locale, $options); 154 | } 155 | 156 | $money = new Money($amount, new Currency(strtoupper($currency ?? config('cashier.currency')))); 157 | 158 | $locale = $locale ?? config('cashier.currency_locale'); 159 | 160 | $numberFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); 161 | 162 | if (isset($options['min_fraction_digits'])) { 163 | $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['min_fraction_digits']); 164 | } 165 | 166 | $moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies()); 167 | 168 | return $moneyFormatter->format($money); 169 | } 170 | 171 | /** 172 | * Configure Cashier to not register its routes. 173 | * 174 | * @return static 175 | */ 176 | public static function ignoreRoutes() 177 | { 178 | static::$registersRoutes = false; 179 | 180 | return new static; 181 | } 182 | 183 | /** 184 | * Configure Cashier to maintain past due subscriptions as active. 185 | * 186 | * @return static 187 | */ 188 | public static function keepPastDueSubscriptionsActive() 189 | { 190 | static::$deactivatePastDue = false; 191 | 192 | return new static; 193 | } 194 | 195 | /** 196 | * Configure Cashier to maintain incomplete subscriptions as active. 197 | * 198 | * @return static 199 | */ 200 | public static function keepIncompleteSubscriptionsActive() 201 | { 202 | static::$deactivateIncomplete = false; 203 | 204 | return new static; 205 | } 206 | 207 | /** 208 | * Configure Cashier to automatically calculate taxes using Stripe Tax. 209 | * 210 | * @return static 211 | */ 212 | public static function calculateTaxes() 213 | { 214 | static::$calculatesTaxes = true; 215 | 216 | return new static; 217 | } 218 | 219 | /** 220 | * Set the customer model class name. 221 | * 222 | * @param string $customerModel 223 | * @return void 224 | */ 225 | public static function useCustomerModel($customerModel) 226 | { 227 | static::$customerModel = $customerModel; 228 | } 229 | 230 | /** 231 | * Set the subscription model class name. 232 | * 233 | * @param string $subscriptionModel 234 | * @return void 235 | */ 236 | public static function useSubscriptionModel($subscriptionModel) 237 | { 238 | static::$subscriptionModel = $subscriptionModel; 239 | } 240 | 241 | /** 242 | * Set the subscription item model class name. 243 | * 244 | * @param string $subscriptionItemModel 245 | * @return void 246 | */ 247 | public static function useSubscriptionItemModel($subscriptionItemModel) 248 | { 249 | static::$subscriptionItemModel = $subscriptionItemModel; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/CashierServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerLogger(); 23 | $this->registerRoutes(); 24 | $this->registerResources(); 25 | $this->registerPublishing(); 26 | $this->registerCommands(); 27 | 28 | Stripe::setAppInfo( 29 | 'Laravel Cashier', 30 | Cashier::VERSION, 31 | 'https://laravel.com' 32 | ); 33 | } 34 | 35 | /** 36 | * Register any application services. 37 | * 38 | * @return void 39 | */ 40 | public function register() 41 | { 42 | $this->configure(); 43 | $this->bindLogger(); 44 | $this->bindInvoiceRenderer(); 45 | } 46 | 47 | /** 48 | * Setup the configuration for Cashier. 49 | * 50 | * @return void 51 | */ 52 | protected function configure() 53 | { 54 | $this->mergeConfigFrom( 55 | __DIR__.'/../config/cashier.php', 'cashier' 56 | ); 57 | } 58 | 59 | /** 60 | * Bind the Stripe logger interface to the Cashier logger. 61 | * 62 | * @return void 63 | */ 64 | protected function bindLogger() 65 | { 66 | $this->app->bind(LoggerInterface::class, function ($app) { 67 | return new Logger( 68 | $app->make('log')->channel(config('cashier.logger')) 69 | ); 70 | }); 71 | } 72 | 73 | /** 74 | * Bind the default invoice renderer. 75 | * 76 | * @return void 77 | */ 78 | protected function bindInvoiceRenderer() 79 | { 80 | $this->app->bind(InvoiceRenderer::class, function ($app) { 81 | return $app->make(config('cashier.invoices.renderer', DompdfInvoiceRenderer::class)); 82 | }); 83 | } 84 | 85 | /** 86 | * Register the Stripe logger. 87 | * 88 | * @return void 89 | */ 90 | protected function registerLogger() 91 | { 92 | if (config('cashier.logger')) { 93 | Stripe::setLogger($this->app->make(LoggerInterface::class)); 94 | } 95 | } 96 | 97 | /** 98 | * Register the package routes. 99 | * 100 | * @return void 101 | */ 102 | protected function registerRoutes() 103 | { 104 | if (Cashier::$registersRoutes) { 105 | Route::group([ 106 | 'prefix' => config('cashier.path'), 107 | 'namespace' => 'Laravel\Cashier\Http\Controllers', 108 | 'as' => 'cashier.', 109 | ], function () { 110 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 111 | }); 112 | } 113 | } 114 | 115 | /** 116 | * Register the package resources. 117 | * 118 | * @return void 119 | */ 120 | protected function registerResources() 121 | { 122 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'cashier'); 123 | } 124 | 125 | /** 126 | * Register the package's publishable resources. 127 | * 128 | * @return void 129 | */ 130 | protected function registerPublishing() 131 | { 132 | if ($this->app->runningInConsole()) { 133 | $this->publishes([ 134 | __DIR__.'/../config/cashier.php' => $this->app->configPath('cashier.php'), 135 | ], 'cashier-config'); 136 | 137 | $publishesMigrationsMethod = method_exists($this, 'publishesMigrations') 138 | ? 'publishesMigrations' 139 | : 'publishes'; 140 | 141 | $this->{$publishesMigrationsMethod}([ 142 | __DIR__.'/../database/migrations' => $this->app->databasePath('migrations'), 143 | ], 'cashier-migrations'); 144 | 145 | $this->publishes([ 146 | __DIR__.'/../resources/views' => $this->app->resourcePath('views/vendor/cashier'), 147 | ], 'cashier-views'); 148 | } 149 | } 150 | 151 | /** 152 | * Register the package's commands. 153 | * 154 | * @return void 155 | */ 156 | protected function registerCommands() 157 | { 158 | if ($this->app->runningInConsole()) { 159 | $this->commands([ 160 | WebhookCommand::class, 161 | ]); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Checkout.php: -------------------------------------------------------------------------------- 1 | owner = $owner; 38 | $this->session = $session; 39 | } 40 | 41 | /** 42 | * Begin a new guest checkout session. 43 | * 44 | * @return \Laravel\Cashier\CheckoutBuilder 45 | */ 46 | public static function guest() 47 | { 48 | return new CheckoutBuilder(); 49 | } 50 | 51 | /** 52 | * Begin a new customer checkout session. 53 | * 54 | * @param \Illuminate\Database\Eloquent\Model $owner 55 | * @param object|null $parentInstance 56 | * @return \Laravel\Cashier\CheckoutBuilder 57 | */ 58 | public static function customer($owner, $parentInstance = null) 59 | { 60 | return new CheckoutBuilder($owner, $parentInstance); 61 | } 62 | 63 | /** 64 | * Begin a new checkout session. 65 | * 66 | * @param \Illuminate\Database\Eloquent\Model|null $owner 67 | * @param array $sessionOptions 68 | * @param array $customerOptions 69 | * @return \Laravel\Cashier\Checkout 70 | */ 71 | public static function create($owner, array $sessionOptions = [], array $customerOptions = []) 72 | { 73 | $data = array_merge([ 74 | 'mode' => Session::MODE_PAYMENT, 75 | ], $sessionOptions); 76 | 77 | if ($owner) { 78 | $data['customer'] = $owner->createOrGetStripeCustomer($customerOptions)->id; 79 | 80 | $stripe = $owner->stripe(); 81 | } else { 82 | $stripe = Cashier::stripe(); 83 | } 84 | 85 | // Make sure to collect address and name when Tax ID collection is enabled... 86 | if (isset($data['customer']) && ($data['tax_id_collection']['enabled'] ?? false)) { 87 | $data['customer_update']['address'] = 'auto'; 88 | $data['customer_update']['name'] = 'auto'; 89 | } 90 | 91 | if ($data['mode'] === Session::MODE_PAYMENT && ($data['invoice_creation']['enabled'] ?? false)) { 92 | $data['invoice_creation']['invoice_data']['metadata']['is_on_session_checkout'] = true; 93 | } elseif ($data['mode'] === Session::MODE_SUBSCRIPTION) { 94 | $data['subscription_data']['metadata']['is_on_session_checkout'] = true; 95 | } 96 | 97 | // Remove success and cancel URLs if "ui_mode" is "embedded"... 98 | if (isset($data['ui_mode']) && $data['ui_mode'] === 'embedded') { 99 | $data['return_url'] = $sessionOptions['return_url'] ?? route('home'); 100 | 101 | // Remove return URL for embedded UI mode when no redirection is desired on completion... 102 | if (isset($data['redirect_on_completion']) && $data['redirect_on_completion'] === 'never') { 103 | unset($data['return_url']); 104 | } 105 | } else { 106 | $data['success_url'] = $sessionOptions['success_url'] ?? route('home').'?checkout=success'; 107 | $data['cancel_url'] = $sessionOptions['cancel_url'] ?? route('home').'?checkout=cancelled'; 108 | } 109 | 110 | $session = $stripe->checkout->sessions->create($data); 111 | 112 | return new static($owner, $session); 113 | } 114 | 115 | /** 116 | * Redirect to the checkout session. 117 | * 118 | * @return \Illuminate\Http\RedirectResponse 119 | */ 120 | public function redirect() 121 | { 122 | return Redirect::to($this->session->url, 303); 123 | } 124 | 125 | /** 126 | * Create an HTTP response that represents the object. 127 | * 128 | * @param \Illuminate\Http\Request $request 129 | * @return \Symfony\Component\HttpFoundation\Response 130 | */ 131 | public function toResponse($request) 132 | { 133 | return $this->redirect(); 134 | } 135 | 136 | /** 137 | * Get the Checkout Session as a Stripe Checkout Session object. 138 | * 139 | * @return \Stripe\Checkout\Session 140 | */ 141 | public function asStripeCheckoutSession() 142 | { 143 | return $this->session; 144 | } 145 | 146 | /** 147 | * Get the instance as an array. 148 | * 149 | * @return array 150 | */ 151 | public function toArray() 152 | { 153 | return $this->asStripeCheckoutSession()->toArray(); 154 | } 155 | 156 | /** 157 | * Convert the object to its JSON representation. 158 | * 159 | * @param int $options 160 | * @return string 161 | */ 162 | public function toJson($options = 0) 163 | { 164 | return json_encode($this->jsonSerialize(), $options); 165 | } 166 | 167 | /** 168 | * Convert the object into something JSON serializable. 169 | * 170 | * @return array 171 | */ 172 | #[\ReturnTypeWillChange] 173 | public function jsonSerialize() 174 | { 175 | return $this->toArray(); 176 | } 177 | 178 | /** 179 | * Dynamically get values from the Stripe object. 180 | * 181 | * @param string $key 182 | * @return mixed 183 | */ 184 | public function __get($key) 185 | { 186 | return $this->session->{$key}; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/CheckoutBuilder.php: -------------------------------------------------------------------------------- 1 | owner = $owner; 31 | 32 | if ($parentInstance && in_array(AllowsCoupons::class, class_uses_recursive($parentInstance))) { 33 | $this->couponId = $parentInstance->couponId; 34 | $this->promotionCodeId = $parentInstance->promotionCodeId; 35 | $this->allowPromotionCodes = $parentInstance->allowPromotionCodes; 36 | } 37 | 38 | if ($parentInstance && in_array(HandlesTaxes::class, class_uses_recursive($parentInstance))) { 39 | $this->customerIpAddress = $parentInstance->customerIpAddress; 40 | $this->estimationBillingAddress = $parentInstance->estimationBillingAddress; 41 | $this->collectTaxIds = $parentInstance->collectTaxIds; 42 | } 43 | } 44 | 45 | /** 46 | * Create a new checkout builder instance. 47 | * 48 | * @param \Illuminate\Database\Eloquent\Model|null $owner 49 | * @param object|null $instance 50 | * @return \Laravel\Cashier\CheckoutBuilder 51 | */ 52 | public static function make($owner = null, $instance = null) 53 | { 54 | return new static($owner, $instance); 55 | } 56 | 57 | /** 58 | * Create a new checkout session. 59 | * 60 | * @param array|string $items 61 | * @param array $sessionOptions 62 | * @param array $customerOptions 63 | * @return \Laravel\Cashier\Checkout 64 | */ 65 | public function create($items, array $sessionOptions = [], array $customerOptions = []) 66 | { 67 | $payload = array_filter([ 68 | 'allow_promotion_codes' => $this->allowPromotionCodes, 69 | 'automatic_tax' => $this->automaticTaxPayload(), 70 | 'discounts' => $this->checkoutDiscounts(), 71 | 'line_items' => Collection::make((array) $items)->map(function ($item, $key) { 72 | if (is_string($key)) { 73 | return ['price' => $key, 'quantity' => $item]; 74 | } 75 | 76 | $item = is_string($item) ? ['price' => $item] : $item; 77 | 78 | $item['quantity'] = $item['quantity'] ?? 1; 79 | 80 | return $item; 81 | })->values()->all(), 82 | 'tax_id_collection' => (Cashier::$calculatesTaxes ?: $this->collectTaxIds) 83 | ? ['enabled' => true] 84 | : [], 85 | ]); 86 | 87 | return Checkout::create($this->owner, array_merge($payload, $sessionOptions), $customerOptions); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Concerns/AllowsCoupons.php: -------------------------------------------------------------------------------- 1 | couponId = $couponId; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * The promotion code ID to apply. 43 | * 44 | * @param string $promotionCodeId 45 | * @return $this 46 | */ 47 | public function withPromotionCode($promotionCodeId) 48 | { 49 | $this->promotionCodeId = $promotionCodeId; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Enables user redeemable promotion codes for a Stripe Checkout session. 56 | * 57 | * @return $this 58 | */ 59 | public function allowPromotionCodes() 60 | { 61 | $this->allowPromotionCodes = true; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Return the discounts for a Stripe Checkout session. 68 | * 69 | * @return array[]|null 70 | */ 71 | protected function checkoutDiscounts() 72 | { 73 | if ($this->couponId) { 74 | return [['coupon' => $this->couponId]]; 75 | } 76 | 77 | if ($this->promotionCodeId) { 78 | return [['promotion_code' => $this->promotionCodeId]]; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Concerns/HandlesPaymentFailures.php: -------------------------------------------------------------------------------- 1 | confirmIncompletePayment && $subscription->hasIncompletePayment()) { 41 | try { 42 | $subscription->latestPayment()->validate(); 43 | } catch (IncompletePayment $e) { 44 | if ($e->payment->requiresConfirmation()) { 45 | try { 46 | if ($paymentMethod) { 47 | $paymentIntent = $e->payment->confirm(array_merge( 48 | $this->paymentConfirmationOptions, 49 | [ 50 | 'expand' => ['invoice.subscription'], 51 | 'payment_method' => $paymentMethod instanceof StripePaymentMethod 52 | ? $paymentMethod->id 53 | : $paymentMethod, 54 | ] 55 | )); 56 | } else { 57 | $paymentIntent = $e->payment->confirm(array_merge( 58 | $this->paymentConfirmationOptions, 59 | ['expand' => ['invoice.subscription']] 60 | )); 61 | } 62 | } catch (StripeCardException) { 63 | $paymentIntent = $e->payment->asStripePaymentIntent(['invoice.subscription']); 64 | } 65 | 66 | $subscription->fill([ 67 | 'stripe_status' => $paymentIntent->invoice->subscription->status, 68 | ])->save(); 69 | 70 | if ($subscription->hasIncompletePayment()) { 71 | (new Payment($paymentIntent))->refresh(['invoice.subscription'])->validate(); 72 | } 73 | } else { 74 | throw $e; 75 | } 76 | } 77 | } 78 | 79 | $this->confirmIncompletePayment = true; 80 | $this->paymentConfirmationOptions = []; 81 | } 82 | 83 | /** 84 | * Prevent automatic confirmation of incomplete payments. 85 | * 86 | * @return $this 87 | */ 88 | public function ignoreIncompletePayments() 89 | { 90 | $this->confirmIncompletePayment = false; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Specify the options to be used when confirming a payment intent. 97 | * 98 | * @param array $options 99 | * @return $this 100 | */ 101 | public function withPaymentConfirmationOptions(array $options) 102 | { 103 | $this->paymentConfirmationOptions = $options; 104 | 105 | return $this; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Concerns/HandlesTaxes.php: -------------------------------------------------------------------------------- 1 | customerIpAddress = $ipAddress; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * Set a pre-collected billing address used to estimate tax rates when performing "one-off" charges. 44 | * 45 | * @param string $country 46 | * @param string|null $postalCode 47 | * @param string|null $state 48 | * @return $this 49 | */ 50 | public function withTaxAddress($country, $postalCode = null, $state = null) 51 | { 52 | $this->estimationBillingAddress = array_filter([ 53 | 'country' => $country, 54 | 'postal_code' => $postalCode, 55 | 'state' => $state, 56 | ]); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Get the payload for Stripe automatic tax calculation. 63 | * 64 | * @return array|null 65 | */ 66 | protected function automaticTaxPayload() 67 | { 68 | return array_filter([ 69 | 'customer_ip_address' => $this->customerIpAddress, 70 | 'enabled' => $this->isAutomaticTaxEnabled(), 71 | 'estimation_billing_address' => $this->estimationBillingAddress, 72 | ]); 73 | } 74 | 75 | /** 76 | * Determine if automatic tax is enabled. 77 | * 78 | * @return bool 79 | */ 80 | protected function isAutomaticTaxEnabled() 81 | { 82 | return Cashier::$calculatesTaxes; 83 | } 84 | 85 | /** 86 | * Indicate that Tax IDs should be collected during a Stripe Checkout session. 87 | * 88 | * @return $this 89 | */ 90 | public function collectTaxIds() 91 | { 92 | $this->collectTaxIds = true; 93 | 94 | return $this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithPaymentBehavior.php: -------------------------------------------------------------------------------- 1 | paymentBehavior = StripeSubscription::PAYMENT_BEHAVIOR_DEFAULT_INCOMPLETE; 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Allow subscription changes even if payment fails. 30 | * 31 | * @return $this 32 | */ 33 | public function allowPaymentFailures() 34 | { 35 | $this->paymentBehavior = StripeSubscription::PAYMENT_BEHAVIOR_ALLOW_INCOMPLETE; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Set any subscription change as pending until payment is successful. 42 | * 43 | * @return $this 44 | */ 45 | public function pendingIfPaymentFails() 46 | { 47 | $this->paymentBehavior = StripeSubscription::PAYMENT_BEHAVIOR_PENDING_IF_INCOMPLETE; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Prevent any subscription change if payment is unsuccessful. 54 | * 55 | * @return $this 56 | */ 57 | public function errorIfPaymentFails() 58 | { 59 | $this->paymentBehavior = StripeSubscription::PAYMENT_BEHAVIOR_ERROR_IF_INCOMPLETE; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Determine the payment behavior when updating the subscription. 66 | * 67 | * @return string 68 | */ 69 | public function paymentBehavior() 70 | { 71 | return $this->paymentBehavior; 72 | } 73 | 74 | /** 75 | * Set the payment behavior for any subscription updates. 76 | * 77 | * @param string $paymentBehavior 78 | * @return $this 79 | */ 80 | public function setPaymentBehavior($paymentBehavior) 81 | { 82 | $this->paymentBehavior = $paymentBehavior; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Concerns/ManagesCustomer.php: -------------------------------------------------------------------------------- 1 | stripe_id; 26 | } 27 | 28 | /** 29 | * Determine if the customer has a Stripe customer ID. 30 | * 31 | * @return bool 32 | */ 33 | public function hasStripeId() 34 | { 35 | return ! is_null($this->stripe_id); 36 | } 37 | 38 | /** 39 | * Determine if the customer has a Stripe customer ID and throw an exception if not. 40 | * 41 | * @return void 42 | * 43 | * @throws \Laravel\Cashier\Exceptions\InvalidCustomer 44 | */ 45 | protected function assertCustomerExists() 46 | { 47 | if (! $this->hasStripeId()) { 48 | throw InvalidCustomer::notYetCreated($this); 49 | } 50 | } 51 | 52 | /** 53 | * Create a Stripe customer for the given model. 54 | * 55 | * @param array $options 56 | * @return \Stripe\Customer 57 | * 58 | * @throws \Laravel\Cashier\Exceptions\CustomerAlreadyCreated 59 | */ 60 | public function createAsStripeCustomer(array $options = []) 61 | { 62 | if ($this->hasStripeId()) { 63 | throw CustomerAlreadyCreated::exists($this); 64 | } 65 | 66 | if (! array_key_exists('name', $options) && $name = $this->stripeName()) { 67 | $options['name'] = $name; 68 | } 69 | 70 | if (! array_key_exists('email', $options) && $email = $this->stripeEmail()) { 71 | $options['email'] = $email; 72 | } 73 | 74 | if (! array_key_exists('phone', $options) && $phone = $this->stripePhone()) { 75 | $options['phone'] = $phone; 76 | } 77 | 78 | if (! array_key_exists('address', $options) && $address = $this->stripeAddress()) { 79 | $options['address'] = $address; 80 | } 81 | 82 | if (! array_key_exists('preferred_locales', $options) && $locales = $this->stripePreferredLocales()) { 83 | $options['preferred_locales'] = $locales; 84 | } 85 | 86 | if (! array_key_exists('metadata', $options) && $metadata = $this->stripeMetadata()) { 87 | $options['metadata'] = $metadata; 88 | } 89 | 90 | // Here we will create the customer instance on Stripe and store the ID of the 91 | // user from Stripe. This ID will correspond with the Stripe user instances 92 | // and allow us to retrieve users from Stripe later when we need to work. 93 | $customer = static::stripe()->customers->create($options); 94 | 95 | $this->stripe_id = $customer->id; 96 | 97 | $this->save(); 98 | 99 | return $customer; 100 | } 101 | 102 | /** 103 | * Update the underlying Stripe customer information for the model. 104 | * 105 | * @param array $options 106 | * @return \Stripe\Customer 107 | */ 108 | public function updateStripeCustomer(array $options = []) 109 | { 110 | return static::stripe()->customers->update( 111 | $this->stripe_id, $options 112 | ); 113 | } 114 | 115 | /** 116 | * Get the Stripe customer instance for the current user or create one. 117 | * 118 | * @param array $options 119 | * @return \Stripe\Customer 120 | */ 121 | public function createOrGetStripeCustomer(array $options = []) 122 | { 123 | if ($this->hasStripeId()) { 124 | return $this->asStripeCustomer($options['expand'] ?? []); 125 | } 126 | 127 | return $this->createAsStripeCustomer($options); 128 | } 129 | 130 | /** 131 | * Update the Stripe customer information for the current user or create one. 132 | * 133 | * @param array $options 134 | * @return \Stripe\Customer 135 | */ 136 | public function updateOrCreateStripeCustomer(array $options = []) 137 | { 138 | if ($this->hasStripeId()) { 139 | return $this->updateStripeCustomer($options); 140 | } 141 | 142 | return $this->createAsStripeCustomer($options); 143 | } 144 | 145 | /** 146 | * Sync the customer's information to Stripe for the current user or create one. 147 | * 148 | * @param array $options 149 | * @return \Stripe\Customer 150 | */ 151 | public function syncOrCreateStripeCustomer(array $options = []) 152 | { 153 | if ($this->hasStripeId()) { 154 | return $this->syncStripeCustomerDetails(); 155 | } 156 | 157 | return $this->createAsStripeCustomer($options); 158 | } 159 | 160 | /** 161 | * Get the Stripe customer for the model. 162 | * 163 | * @param array $expand 164 | * @return \Stripe\Customer 165 | */ 166 | public function asStripeCustomer(array $expand = []) 167 | { 168 | $this->assertCustomerExists(); 169 | 170 | return static::stripe()->customers->retrieve( 171 | $this->stripe_id, ['expand' => $expand] 172 | ); 173 | } 174 | 175 | /** 176 | * Get the name that should be synced to Stripe. 177 | * 178 | * @return string|null 179 | */ 180 | public function stripeName() 181 | { 182 | return $this->name ?? null; 183 | } 184 | 185 | /** 186 | * Get the email address that should be synced to Stripe. 187 | * 188 | * @return string|null 189 | */ 190 | public function stripeEmail() 191 | { 192 | return $this->email ?? null; 193 | } 194 | 195 | /** 196 | * Get the phone number that should be synced to Stripe. 197 | * 198 | * @return string|null 199 | */ 200 | public function stripePhone() 201 | { 202 | return $this->phone ?? null; 203 | } 204 | 205 | /** 206 | * Get the address that should be synced to Stripe. 207 | * 208 | * @return array|null 209 | */ 210 | public function stripeAddress() 211 | { 212 | return []; 213 | 214 | // return [ 215 | // 'city' => 'Little Rock', 216 | // 'country' => 'US', 217 | // 'line1' => '1 Main St.', 218 | // 'line2' => 'Apartment 5', 219 | // 'postal_code' => '72201', 220 | // 'state' => 'Arkansas', 221 | // ]; 222 | } 223 | 224 | /** 225 | * Get the locales that should be synced to Stripe. 226 | * 227 | * @return array|null 228 | */ 229 | public function stripePreferredLocales() 230 | { 231 | return []; 232 | 233 | // return ['en']; 234 | } 235 | 236 | /** 237 | * Get the metadata that should be synced to Stripe. 238 | * 239 | * @return array|null 240 | */ 241 | public function stripeMetadata() 242 | { 243 | return []; 244 | } 245 | 246 | /** 247 | * Sync the customer's information to Stripe. 248 | * 249 | * @return \Stripe\Customer 250 | */ 251 | public function syncStripeCustomerDetails() 252 | { 253 | return $this->updateStripeCustomer([ 254 | 'name' => $this->stripeName(), 255 | 'email' => $this->stripeEmail(), 256 | 'phone' => $this->stripePhone(), 257 | 'address' => $this->stripeAddress(), 258 | 'preferred_locales' => $this->stripePreferredLocales(), 259 | 'metadata' => $this->stripeMetadata(), 260 | ]); 261 | } 262 | 263 | /** 264 | * The discount that applies to the customer, if applicable. 265 | * 266 | * @return \Laravel\Cashier\Discount|null 267 | */ 268 | public function discount() 269 | { 270 | $customer = $this->asStripeCustomer(['discount.promotion_code']); 271 | 272 | return $customer->discount 273 | ? new Discount($customer->discount) 274 | : null; 275 | } 276 | 277 | /** 278 | * Apply a coupon to the customer. 279 | * 280 | * @param string $coupon 281 | * @return void 282 | */ 283 | public function applyCoupon($coupon) 284 | { 285 | $this->assertCustomerExists(); 286 | 287 | $this->updateStripeCustomer([ 288 | 'coupon' => $coupon, 289 | ]); 290 | } 291 | 292 | /** 293 | * Apply a promotion code to the customer. 294 | * 295 | * @param string $promotionCodeId 296 | * @return void 297 | */ 298 | public function applyPromotionCode($promotionCodeId) 299 | { 300 | $this->assertCustomerExists(); 301 | 302 | $this->updateStripeCustomer([ 303 | 'promotion_code' => $promotionCodeId, 304 | ]); 305 | } 306 | 307 | /** 308 | * Retrieve a promotion code by its code. 309 | * 310 | * @param string $code 311 | * @param array $options 312 | * @return \Laravel\Cashier\PromotionCode|null 313 | */ 314 | public function findPromotionCode($code, array $options = []) 315 | { 316 | $codes = static::stripe()->promotionCodes->all(array_merge([ 317 | 'code' => $code, 318 | 'limit' => 1, 319 | ], $options)); 320 | 321 | if ($codes && $promotionCode = $codes->first()) { 322 | return new PromotionCode($promotionCode); 323 | } 324 | } 325 | 326 | /** 327 | * Retrieve a promotion code by its code. 328 | * 329 | * @param string $code 330 | * @param array $options 331 | * @return \Laravel\Cashier\PromotionCode|null 332 | */ 333 | public function findActivePromotionCode($code, array $options = []) 334 | { 335 | return $this->findPromotionCode($code, array_merge($options, ['active' => true])); 336 | } 337 | 338 | /** 339 | * Get the total balance of the customer. 340 | * 341 | * @return string 342 | */ 343 | public function balance() 344 | { 345 | return $this->formatAmount($this->rawBalance()); 346 | } 347 | 348 | /** 349 | * Get the raw total balance of the customer. 350 | * 351 | * @return int 352 | */ 353 | public function rawBalance() 354 | { 355 | if (! $this->hasStripeId()) { 356 | return 0; 357 | } 358 | 359 | return $this->asStripeCustomer()->balance; 360 | } 361 | 362 | /** 363 | * Return a customer's balance transactions. 364 | * 365 | * @param int $limit 366 | * @param array $options 367 | * @return \Illuminate\Support\Collection 368 | */ 369 | public function balanceTransactions($limit = 10, array $options = []) 370 | { 371 | if (! $this->hasStripeId()) { 372 | return new Collection(); 373 | } 374 | 375 | $transactions = static::stripe() 376 | ->customers 377 | ->allBalanceTransactions($this->stripe_id, array_merge(['limit' => $limit], $options)); 378 | 379 | return Collection::make($transactions->data)->map(function ($transaction) { 380 | return new CustomerBalanceTransaction($this, $transaction); 381 | }); 382 | } 383 | 384 | /** 385 | * Credit a customer's balance. 386 | * 387 | * @param int $amount 388 | * @param string|null $description 389 | * @param array $options 390 | * @return \Laravel\Cashier\CustomerBalanceTransaction 391 | */ 392 | public function creditBalance($amount, $description = null, array $options = []) 393 | { 394 | return $this->applyBalance(-$amount, $description, $options); 395 | } 396 | 397 | /** 398 | * Debit a customer's balance. 399 | * 400 | * @param int $amount 401 | * @param string|null $description 402 | * @param array $options 403 | * @return \Laravel\Cashier\CustomerBalanceTransaction 404 | */ 405 | public function debitBalance($amount, $description = null, array $options = []) 406 | { 407 | return $this->applyBalance($amount, $description, $options); 408 | } 409 | 410 | /** 411 | * Apply a new amount to the customer's balance. 412 | * 413 | * @param int $amount 414 | * @param string|null $description 415 | * @param array $options 416 | * @return \Laravel\Cashier\CustomerBalanceTransaction 417 | */ 418 | public function applyBalance($amount, $description = null, array $options = []) 419 | { 420 | $this->assertCustomerExists(); 421 | 422 | $transaction = static::stripe() 423 | ->customers 424 | ->createBalanceTransaction($this->stripe_id, array_filter(array_merge([ 425 | 'amount' => $amount, 426 | 'currency' => $this->preferredCurrency(), 427 | 'description' => $description, 428 | ], $options))); 429 | 430 | return new CustomerBalanceTransaction($this, $transaction); 431 | } 432 | 433 | /** 434 | * Get the Stripe supported currency used by the customer. 435 | * 436 | * @return string 437 | */ 438 | public function preferredCurrency() 439 | { 440 | return config('cashier.currency'); 441 | } 442 | 443 | /** 444 | * Format the given amount into a displayable currency. 445 | * 446 | * @param int $amount 447 | * @return string 448 | */ 449 | protected function formatAmount($amount) 450 | { 451 | return Cashier::formatAmount($amount, $this->preferredCurrency()); 452 | } 453 | 454 | /** 455 | * Get the Stripe billing portal for this customer. 456 | * 457 | * @param string|null $returnUrl 458 | * @param array $options 459 | * @return string 460 | */ 461 | public function billingPortalUrl($returnUrl = null, array $options = []) 462 | { 463 | $this->assertCustomerExists(); 464 | 465 | return static::stripe()->billingPortal->sessions->create(array_merge([ 466 | 'customer' => $this->stripeId(), 467 | 'return_url' => $returnUrl ?? route('home'), 468 | ], $options))['url']; 469 | } 470 | 471 | /** 472 | * Generate a redirect response to the customer's Stripe billing portal. 473 | * 474 | * @param string|null $returnUrl 475 | * @param array $options 476 | * @return \Illuminate\Http\RedirectResponse 477 | */ 478 | public function redirectToBillingPortal($returnUrl = null, array $options = []) 479 | { 480 | return new RedirectResponse( 481 | $this->billingPortalUrl($returnUrl, $options) 482 | ); 483 | } 484 | 485 | /** 486 | * Get a collection of the customer's TaxID's. 487 | * 488 | * @param array $options 489 | * @return \Illuminate\Support\Collection|\Stripe\TaxId[] 490 | */ 491 | public function taxIds(array $options = []) 492 | { 493 | $this->assertCustomerExists(); 494 | 495 | return new Collection( 496 | static::stripe()->customers->allTaxIds($this->stripe_id, $options)->data 497 | ); 498 | } 499 | 500 | /** 501 | * Find a TaxID by ID. 502 | * 503 | * @param string $id 504 | * @return \Stripe\TaxId|null 505 | */ 506 | public function findTaxId($id) 507 | { 508 | $this->assertCustomerExists(); 509 | 510 | try { 511 | return static::stripe()->customers->retrieveTaxId( 512 | $this->stripe_id, $id, [] 513 | ); 514 | } catch (StripeInvalidRequestException $exception) { 515 | // 516 | } 517 | } 518 | 519 | /** 520 | * Create a TaxID for the customer. 521 | * 522 | * @param string $type 523 | * @param string $value 524 | * @return \Stripe\TaxId 525 | */ 526 | public function createTaxId($type, $value) 527 | { 528 | $this->assertCustomerExists(); 529 | 530 | return static::stripe()->customers->createTaxId($this->stripe_id, [ 531 | 'type' => $type, 532 | 'value' => $value, 533 | ]); 534 | } 535 | 536 | /** 537 | * Delete a TaxID for the customer. 538 | * 539 | * @param string $id 540 | * @return void 541 | */ 542 | public function deleteTaxId($id) 543 | { 544 | $this->assertCustomerExists(); 545 | 546 | try { 547 | static::stripe()->customers->deleteTaxId($this->stripe_id, $id); 548 | } catch (StripeInvalidRequestException $exception) { 549 | // 550 | } 551 | } 552 | 553 | /** 554 | * Determine if the customer is not exempted from taxes. 555 | * 556 | * @return bool 557 | */ 558 | public function isNotTaxExempt() 559 | { 560 | return $this->asStripeCustomer()->tax_exempt === StripeCustomer::TAX_EXEMPT_NONE; 561 | } 562 | 563 | /** 564 | * Determine if the customer is exempted from taxes. 565 | * 566 | * @return bool 567 | */ 568 | public function isTaxExempt() 569 | { 570 | return $this->asStripeCustomer()->tax_exempt === StripeCustomer::TAX_EXEMPT_EXEMPT; 571 | } 572 | 573 | /** 574 | * Determine if reverse charge applies to the customer. 575 | * 576 | * @return bool 577 | */ 578 | public function reverseChargeApplies() 579 | { 580 | return $this->asStripeCustomer()->tax_exempt === StripeCustomer::TAX_EXEMPT_REVERSE; 581 | } 582 | 583 | /** 584 | * Get the Stripe SDK client. 585 | * 586 | * @param array $options 587 | * @return \Stripe\StripeClient 588 | */ 589 | public static function stripe(array $options = []) 590 | { 591 | return Cashier::stripe($options); 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /src/Concerns/ManagesInvoices.php: -------------------------------------------------------------------------------- 1 | isAutomaticTaxEnabled() && ! array_key_exists('price_data', $options)) { 32 | throw new LogicException( 33 | 'When using automatic tax calculation, you must include "price_data" in the provided options array.' 34 | ); 35 | } 36 | 37 | $this->assertCustomerExists(); 38 | 39 | $options = array_merge([ 40 | 'customer' => $this->stripe_id, 41 | 'currency' => $this->preferredCurrency(), 42 | 'description' => $description, 43 | ], $options); 44 | 45 | if (array_key_exists('price_data', $options)) { 46 | $options['price_data'] = array_merge([ 47 | 'unit_amount' => $amount, 48 | 'currency' => $this->preferredCurrency(), 49 | ], $options['price_data']); 50 | } elseif (array_key_exists('quantity', $options)) { 51 | $options['unit_amount'] = $options['unit_amount'] ?? $amount; 52 | } else { 53 | $options['amount'] = $amount; 54 | } 55 | 56 | return static::stripe()->invoiceItems->create($options); 57 | } 58 | 59 | /** 60 | * Invoice the customer for the given amount and generate an invoice immediately. 61 | * 62 | * @param string $description 63 | * @param int $amount 64 | * @param array $tabOptions 65 | * @param array $invoiceOptions 66 | * @return \Laravel\Cashier\Invoice 67 | * 68 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 69 | */ 70 | public function invoiceFor($description, $amount, array $tabOptions = [], array $invoiceOptions = []) 71 | { 72 | $this->tab($description, $amount, $tabOptions); 73 | 74 | return $this->invoice($invoiceOptions); 75 | } 76 | 77 | /** 78 | * Add an invoice item for a specific Price ID to the customer's upcoming invoice. 79 | * 80 | * @param string $price 81 | * @param int $quantity 82 | * @param array $options 83 | * @return \Stripe\InvoiceItem 84 | */ 85 | public function tabPrice($price, $quantity = 1, array $options = []) 86 | { 87 | $this->assertCustomerExists(); 88 | 89 | $options = array_merge([ 90 | 'customer' => $this->stripe_id, 91 | 'price' => $price, 92 | 'quantity' => $quantity, 93 | ], $options); 94 | 95 | return static::stripe()->invoiceItems->create($options); 96 | } 97 | 98 | /** 99 | * Invoice the customer for the given Price ID and generate an invoice immediately. 100 | * 101 | * @param string $price 102 | * @param int $quantity 103 | * @param array $tabOptions 104 | * @param array $invoiceOptions 105 | * @return \Laravel\Cashier\Invoice 106 | * 107 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 108 | */ 109 | public function invoicePrice($price, $quantity = 1, array $tabOptions = [], array $invoiceOptions = []) 110 | { 111 | $this->tabPrice($price, $quantity, $tabOptions); 112 | 113 | return $this->invoice($invoiceOptions); 114 | } 115 | 116 | /** 117 | * Invoice the customer outside of the regular billing cycle. 118 | * 119 | * @param array $options 120 | * @return \Laravel\Cashier\Invoice 121 | * 122 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 123 | */ 124 | public function invoice(array $options = []) 125 | { 126 | try { 127 | $payOptions = Arr::only($options, $payOptionKeys = [ 128 | 'forgive', 129 | 'mandate', 130 | 'off_session', 131 | 'paid_out_of_band', 132 | 'payment_method', 133 | 'source', 134 | ]); 135 | 136 | Arr::forget($options, $payOptionKeys); 137 | 138 | $invoice = $this->createInvoice(array_merge([ 139 | 'pending_invoice_items_behavior' => 'include', 140 | ], $options)); 141 | 142 | return $invoice->chargesAutomatically() ? $invoice->pay($payOptions) : $invoice->send(); 143 | } catch (StripeCardException) { 144 | $payment = new Payment( 145 | static::stripe()->paymentIntents->retrieve( 146 | $invoice->asStripeInvoice()->refresh()->payment_intent, 147 | ['expand' => ['invoice.subscription']] 148 | ) 149 | ); 150 | 151 | $payment->validate(); 152 | } 153 | } 154 | 155 | /** 156 | * Create an invoice within Stripe. 157 | * 158 | * @param array $options 159 | * @return \Laravel\Cashier\Invoice 160 | */ 161 | public function createInvoice(array $options = []) 162 | { 163 | $this->assertCustomerExists(); 164 | 165 | $stripeCustomer = $this->asStripeCustomer(); 166 | 167 | $parameters = array_merge([ 168 | 'automatic_tax' => $this->automaticTaxPayload(), 169 | 'customer' => $this->stripe_id, 170 | 'currency' => $stripeCustomer->currency ?? config('cashier.currency'), 171 | ], $options); 172 | 173 | if (isset($parameters['subscription'])) { 174 | unset($parameters['currency']); 175 | } 176 | 177 | if (array_key_exists('subscription', $parameters)) { 178 | unset($parameters['pending_invoice_items_behavior']); 179 | } 180 | 181 | $stripeInvoice = static::stripe()->invoices->create($parameters); 182 | 183 | return new Invoice($this, $stripeInvoice); 184 | } 185 | 186 | /** 187 | * Get the customer's upcoming invoice. 188 | * 189 | * @param array $options 190 | * @return \Laravel\Cashier\Invoice|null 191 | */ 192 | public function upcomingInvoice(array $options = []) 193 | { 194 | if (! $this->hasStripeId()) { 195 | return; 196 | } 197 | 198 | $parameters = array_merge([ 199 | 'automatic_tax' => $this->automaticTaxPayload(), 200 | 'customer' => $this->stripe_id, 201 | ], $options); 202 | 203 | try { 204 | $stripeInvoice = static::stripe()->invoices->upcoming($parameters); 205 | 206 | return new Invoice($this, $stripeInvoice, $parameters); 207 | } catch (StripeInvalidRequestException $exception) { 208 | // 209 | } 210 | } 211 | 212 | /** 213 | * Find an invoice by ID. 214 | * 215 | * @param string $id 216 | * @return \Laravel\Cashier\Invoice|null 217 | */ 218 | public function findInvoice($id) 219 | { 220 | $stripeInvoice = null; 221 | 222 | try { 223 | $stripeInvoice = static::stripe()->invoices->retrieve($id); 224 | } catch (StripeInvalidRequestException $exception) { 225 | // 226 | } 227 | 228 | return $stripeInvoice ? new Invoice($this, $stripeInvoice) : null; 229 | } 230 | 231 | /** 232 | * Find an invoice or throw a 404 or 403 error. 233 | * 234 | * @param string $id 235 | * @return \Laravel\Cashier\Invoice 236 | * 237 | * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException 238 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 239 | */ 240 | public function findInvoiceOrFail($id) 241 | { 242 | try { 243 | $invoice = $this->findInvoice($id); 244 | } catch (InvalidInvoice $exception) { 245 | throw new AccessDeniedHttpException; 246 | } 247 | 248 | if (is_null($invoice)) { 249 | throw new NotFoundHttpException; 250 | } 251 | 252 | return $invoice; 253 | } 254 | 255 | /** 256 | * Create an invoice download Response. 257 | * 258 | * @param string $id 259 | * @param array $data 260 | * @param string $filename 261 | * @return \Symfony\Component\HttpFoundation\Response 262 | */ 263 | public function downloadInvoice($id, array $data = [], $filename = null) 264 | { 265 | $invoice = $this->findInvoiceOrFail($id); 266 | 267 | return $filename ? $invoice->downloadAs($filename, $data) : $invoice->download($data); 268 | } 269 | 270 | /** 271 | * Get a collection of the customer's invoices. 272 | * 273 | * @param bool $includePending 274 | * @param array $parameters 275 | * @return \Illuminate\Support\Collection|\Laravel\Cashier\Invoice[] 276 | */ 277 | public function invoices($includePending = false, $parameters = []) 278 | { 279 | if (! $this->hasStripeId()) { 280 | return new Collection(); 281 | } 282 | 283 | $invoices = []; 284 | 285 | $parameters = array_merge(['limit' => 24], $parameters); 286 | 287 | $stripeInvoices = static::stripe()->invoices->all( 288 | ['customer' => $this->stripe_id] + $parameters 289 | ); 290 | 291 | // Here we will loop through the Stripe invoices and create our own custom Invoice 292 | // instances that have more helper methods and are generally more convenient to 293 | // work with than the plain Stripe objects are. Then, we'll return the array. 294 | if (! is_null($stripeInvoices)) { 295 | foreach ($stripeInvoices->data as $invoice) { 296 | if ($invoice->paid || $includePending) { 297 | $invoices[] = new Invoice($this, $invoice); 298 | } 299 | } 300 | } 301 | 302 | return new Collection($invoices); 303 | } 304 | 305 | /** 306 | * Get an array of the customer's invoices, including pending invoices. 307 | * 308 | * @param array $parameters 309 | * @return \Illuminate\Support\Collection|\Laravel\Cashier\Invoice[] 310 | */ 311 | public function invoicesIncludingPending(array $parameters = []) 312 | { 313 | return $this->invoices(true, $parameters); 314 | } 315 | 316 | /** 317 | * Get a cursor paginator for the customer's invoices. 318 | * 319 | * @param int|null $perPage 320 | * @param array $parameters 321 | * @param string $cursorName 322 | * @param \Illuminate\Pagination\Cursor|string|null $cursor 323 | * @return \Illuminate\Contracts\Pagination\CursorPaginator 324 | */ 325 | public function cursorPaginateInvoices($perPage = 24, array $parameters = [], $cursorName = 'cursor', $cursor = null) 326 | { 327 | if (! $cursor instanceof Cursor) { 328 | $cursor = is_string($cursor) 329 | ? Cursor::fromEncoded($cursor) 330 | : CursorPaginator::resolveCurrentCursor($cursorName, $cursor); 331 | } 332 | 333 | if (! is_null($cursor)) { 334 | if ($cursor->pointsToNextItems()) { 335 | $parameters['starting_after'] = $cursor->parameter('id'); 336 | } else { 337 | $parameters['ending_before'] = $cursor->parameter('id'); 338 | } 339 | } 340 | 341 | $invoices = $this->invoices(true, array_merge($parameters, ['limit' => $perPage + 1])); 342 | 343 | if (! is_null($cursor) && $cursor->pointsToPreviousItems()) { 344 | $invoices = $invoices->reverse(); 345 | } 346 | 347 | return new CursorPaginator($invoices, $perPage, $cursor, array_merge([ 348 | 'path' => Paginator::resolveCurrentPath(), 349 | 'cursorName' => $cursorName, 350 | 'parameters' => ['id'], 351 | ])); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Concerns/ManagesPaymentMethods.php: -------------------------------------------------------------------------------- 1 | hasStripeId()) { 23 | $options['customer'] = $this->stripe_id; 24 | } 25 | 26 | return static::stripe()->setupIntents->create($options); 27 | } 28 | 29 | /** 30 | * Retrieve a SetupIntent from Stripe. 31 | * 32 | * @param string $id 33 | * @param array $params 34 | * @param array $options 35 | * @return \Stripe\SetupIntent 36 | */ 37 | public function findSetupIntent(string $id, array $params = [], array $options = []) 38 | { 39 | return static::stripe()->setupIntents->retrieve($id, $params, $options); 40 | } 41 | 42 | /** 43 | * Determines if the customer currently has a default payment method. 44 | * 45 | * @return bool 46 | */ 47 | public function hasDefaultPaymentMethod() 48 | { 49 | return (bool) $this->pm_type; 50 | } 51 | 52 | /** 53 | * Determines if the customer currently has at least one payment method of an optional type. 54 | * 55 | * @param string|null $type 56 | * @return bool 57 | */ 58 | public function hasPaymentMethod($type = null) 59 | { 60 | return $this->paymentMethods($type)->isNotEmpty(); 61 | } 62 | 63 | /** 64 | * Get a collection of the customer's payment methods of an optional type. 65 | * 66 | * @param string|null $type 67 | * @param array $parameters 68 | * @return \Illuminate\Support\Collection|\Laravel\Cashier\PaymentMethod[] 69 | */ 70 | public function paymentMethods($type = null, $parameters = []) 71 | { 72 | if (! $this->hasStripeId()) { 73 | return new Collection(); 74 | } 75 | 76 | $parameters = array_merge(['limit' => 24], $parameters); 77 | 78 | // "type" is temporarily required by Stripe... 79 | $paymentMethods = static::stripe()->paymentMethods->all( 80 | array_filter(['customer' => $this->stripe_id, 'type' => $type]) + $parameters 81 | ); 82 | 83 | return Collection::make($paymentMethods->data)->map(function ($paymentMethod) { 84 | return new PaymentMethod($this, $paymentMethod); 85 | }); 86 | } 87 | 88 | /** 89 | * Add a payment method to the customer. 90 | * 91 | * @param \Stripe\PaymentMethod|string $paymentMethod 92 | * @return \Laravel\Cashier\PaymentMethod 93 | */ 94 | public function addPaymentMethod($paymentMethod) 95 | { 96 | $this->assertCustomerExists(); 97 | 98 | $stripePaymentMethod = $this->resolveStripePaymentMethod($paymentMethod); 99 | 100 | if ($stripePaymentMethod->customer !== $this->stripe_id) { 101 | $stripePaymentMethod = $stripePaymentMethod->attach( 102 | ['customer' => $this->stripe_id] 103 | ); 104 | } 105 | 106 | return new PaymentMethod($this, $stripePaymentMethod); 107 | } 108 | 109 | /** 110 | * Delete a payment method from the customer. 111 | * 112 | * @param \Stripe\PaymentMethod|string $paymentMethod 113 | * @return void 114 | */ 115 | public function deletePaymentMethod($paymentMethod) 116 | { 117 | $this->assertCustomerExists(); 118 | 119 | $stripePaymentMethod = $this->resolveStripePaymentMethod($paymentMethod); 120 | 121 | if ($stripePaymentMethod->customer !== $this->stripe_id) { 122 | return; 123 | } 124 | 125 | $customer = $this->asStripeCustomer(); 126 | 127 | $defaultPaymentMethod = $customer->invoice_settings->default_payment_method; 128 | 129 | $stripePaymentMethod->detach(); 130 | 131 | // If the payment method was the default payment method, we'll remove it manually... 132 | if ($stripePaymentMethod->id === $defaultPaymentMethod) { 133 | $this->forceFill([ 134 | 'pm_type' => null, 135 | 'pm_last_four' => null, 136 | ])->save(); 137 | } 138 | } 139 | 140 | /** 141 | * Get the default payment method for the customer. 142 | * 143 | * @return \Laravel\Cashier\PaymentMethod|\Stripe\Card|\Stripe\BankAccount|null 144 | */ 145 | public function defaultPaymentMethod() 146 | { 147 | if (! $this->hasStripeId()) { 148 | return; 149 | } 150 | 151 | /** @var \Stripe\Customer */ 152 | $customer = $this->asStripeCustomer(['default_source', 'invoice_settings.default_payment_method']); 153 | 154 | if ($customer->invoice_settings?->default_payment_method) { 155 | return new PaymentMethod($this, $customer->invoice_settings->default_payment_method); 156 | } 157 | 158 | // If we can't find a payment method, try to return a legacy source... 159 | return $customer->default_source; 160 | } 161 | 162 | /** 163 | * Update customer's default payment method. 164 | * 165 | * @param \Stripe\PaymentMethod|string $paymentMethod 166 | * @return \Laravel\Cashier\PaymentMethod 167 | */ 168 | public function updateDefaultPaymentMethod($paymentMethod) 169 | { 170 | $this->assertCustomerExists(); 171 | 172 | $customer = $this->asStripeCustomer(); 173 | 174 | $stripePaymentMethod = $this->resolveStripePaymentMethod($paymentMethod); 175 | 176 | // If the customer already has the payment method as their default, we can bail out 177 | // of the call now. We don't need to keep adding the same payment method to this 178 | // model's account every single time we go through this specific process call. 179 | if ($stripePaymentMethod->id === $customer->invoice_settings->default_payment_method) { 180 | return; 181 | } 182 | 183 | $paymentMethod = $this->addPaymentMethod($stripePaymentMethod); 184 | 185 | $this->updateStripeCustomer([ 186 | 'invoice_settings' => ['default_payment_method' => $paymentMethod->id], 187 | ]); 188 | 189 | // Next we will get the default payment method for this user so we can update the 190 | // payment method details on the record in the database. This will allow us to 191 | // show that information on the front-end when updating the payment methods. 192 | $this->fillPaymentMethodDetails($paymentMethod); 193 | 194 | $this->save(); 195 | 196 | return $paymentMethod; 197 | } 198 | 199 | /** 200 | * Synchronises the customer's default payment method from Stripe back into the database. 201 | * 202 | * @return $this 203 | */ 204 | public function updateDefaultPaymentMethodFromStripe() 205 | { 206 | $defaultPaymentMethod = $this->defaultPaymentMethod(); 207 | 208 | if ($defaultPaymentMethod) { 209 | if ($defaultPaymentMethod instanceof PaymentMethod) { 210 | $this->fillPaymentMethodDetails( 211 | $defaultPaymentMethod->asStripePaymentMethod() 212 | )->save(); 213 | } else { 214 | $this->fillSourceDetails($defaultPaymentMethod)->save(); 215 | } 216 | } else { 217 | $this->forceFill([ 218 | 'pm_type' => null, 219 | 'pm_last_four' => null, 220 | ])->save(); 221 | } 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * Fills the model's properties with the payment method from Stripe. 228 | * 229 | * @param \Laravel\Cashier\PaymentMethod|\Stripe\PaymentMethod|null $paymentMethod 230 | * @return $this 231 | */ 232 | protected function fillPaymentMethodDetails($paymentMethod) 233 | { 234 | if ($paymentMethod->type === 'card') { 235 | $this->pm_type = $paymentMethod->card->brand; 236 | $this->pm_last_four = $paymentMethod->card->last4; 237 | } else { 238 | $this->pm_type = $type = $paymentMethod->type; 239 | $this->pm_last_four = $paymentMethod?->$type->last4 ?? null; 240 | } 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * Fills the model's properties with the source from Stripe. 247 | * 248 | * @param \Stripe\Card|\Stripe\BankAccount|null $source 249 | * @return $this 250 | * 251 | * @deprecated Will be removed in a future Cashier update. You should use the new payment methods API instead. 252 | */ 253 | protected function fillSourceDetails($source) 254 | { 255 | if ($source instanceof StripeCard) { 256 | $this->pm_type = $source->brand; 257 | $this->pm_last_four = $source->last4; 258 | } elseif ($source instanceof StripeBankAccount) { 259 | $this->pm_type = 'Bank Account'; 260 | $this->pm_last_four = $source->last4; 261 | } 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Deletes the customer's payment methods of the given type. 268 | * 269 | * @param string|null $type 270 | * @return void 271 | */ 272 | public function deletePaymentMethods($type = null) 273 | { 274 | $this->paymentMethods($type)->each(function (PaymentMethod $paymentMethod) { 275 | $paymentMethod->delete(); 276 | }); 277 | 278 | $this->updateDefaultPaymentMethodFromStripe(); 279 | } 280 | 281 | /** 282 | * Find a PaymentMethod by ID. 283 | * 284 | * @param string $paymentMethod 285 | * @return \Laravel\Cashier\PaymentMethod|null 286 | */ 287 | public function findPaymentMethod($paymentMethod) 288 | { 289 | $stripePaymentMethod = null; 290 | 291 | try { 292 | $stripePaymentMethod = $this->resolveStripePaymentMethod($paymentMethod); 293 | } catch (Exception $exception) { 294 | // 295 | } 296 | 297 | return $stripePaymentMethod ? new PaymentMethod($this, $stripePaymentMethod) : null; 298 | } 299 | 300 | /** 301 | * Resolve a PaymentMethod ID to a Stripe PaymentMethod object. 302 | * 303 | * @param \Stripe\PaymentMethod|string $paymentMethod 304 | * @return \Stripe\PaymentMethod 305 | */ 306 | protected function resolveStripePaymentMethod($paymentMethod) 307 | { 308 | if ($paymentMethod instanceof StripePaymentMethod) { 309 | return $paymentMethod; 310 | } 311 | 312 | return static::stripe()->paymentMethods->retrieve($paymentMethod); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Concerns/ManagesSubscriptions.php: -------------------------------------------------------------------------------- 1 | onGenericTrial()) { 34 | return true; 35 | } 36 | 37 | $subscription = $this->subscription($type); 38 | 39 | if (! $subscription || ! $subscription->onTrial()) { 40 | return false; 41 | } 42 | 43 | return ! $price || $subscription->hasPrice($price); 44 | } 45 | 46 | /** 47 | * Determine if the Stripe model's trial has ended. 48 | * 49 | * @param string $type 50 | * @param string|null $price 51 | * @return bool 52 | */ 53 | public function hasExpiredTrial($type = 'default', $price = null) 54 | { 55 | if (func_num_args() === 0 && $this->hasExpiredGenericTrial()) { 56 | return true; 57 | } 58 | 59 | $subscription = $this->subscription($type); 60 | 61 | if (! $subscription || ! $subscription->hasExpiredTrial()) { 62 | return false; 63 | } 64 | 65 | return ! $price || $subscription->hasPrice($price); 66 | } 67 | 68 | /** 69 | * Determine if the Stripe model is on a "generic" trial at the model level. 70 | * 71 | * @return bool 72 | */ 73 | public function onGenericTrial() 74 | { 75 | return $this->trial_ends_at && $this->trial_ends_at->isFuture(); 76 | } 77 | 78 | /** 79 | * Filter the given query for generic trials. 80 | * 81 | * @param \Illuminate\Database\Eloquent\Builder $query 82 | * @return void 83 | */ 84 | public function scopeOnGenericTrial($query) 85 | { 86 | $query->whereNotNull('trial_ends_at')->where('trial_ends_at', '>', Carbon::now()); 87 | } 88 | 89 | /** 90 | * Determine if the Stripe model's "generic" trial at the model level has expired. 91 | * 92 | * @return bool 93 | */ 94 | public function hasExpiredGenericTrial() 95 | { 96 | return $this->trial_ends_at && $this->trial_ends_at->isPast(); 97 | } 98 | 99 | /** 100 | * Filter the given query for expired generic trials. 101 | * 102 | * @param \Illuminate\Database\Eloquent\Builder $query 103 | * @return void 104 | */ 105 | public function scopeHasExpiredGenericTrial($query) 106 | { 107 | $query->whereNotNull('trial_ends_at')->where('trial_ends_at', '<', Carbon::now()); 108 | } 109 | 110 | /** 111 | * Get the ending date of the trial. 112 | * 113 | * @param string $type 114 | * @return \Illuminate\Support\Carbon|null 115 | */ 116 | public function trialEndsAt($type = 'default') 117 | { 118 | if (func_num_args() === 0 && $this->onGenericTrial()) { 119 | return $this->trial_ends_at; 120 | } 121 | 122 | if ($subscription = $this->subscription($type)) { 123 | return $subscription->trial_ends_at; 124 | } 125 | 126 | return $this->trial_ends_at; 127 | } 128 | 129 | /** 130 | * Determine if the Stripe model has a given subscription. 131 | * 132 | * @param string $type 133 | * @param string|null $price 134 | * @return bool 135 | */ 136 | public function subscribed($type = 'default', $price = null) 137 | { 138 | $subscription = $this->subscription($type); 139 | 140 | if (! $subscription || ! $subscription->valid()) { 141 | return false; 142 | } 143 | 144 | return ! $price || $subscription->hasPrice($price); 145 | } 146 | 147 | /** 148 | * Get a subscription instance by $type. 149 | * 150 | * @param string $type 151 | * @return \Laravel\Cashier\Subscription|null 152 | */ 153 | public function subscription($type = 'default') 154 | { 155 | return $this->subscriptions->where('type', $type)->first(); 156 | } 157 | 158 | /** 159 | * Get all of the subscriptions for the Stripe model. 160 | * 161 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 162 | */ 163 | public function subscriptions() 164 | { 165 | return $this->hasMany(Cashier::$subscriptionModel, $this->getForeignKey())->orderBy('created_at', 'desc'); 166 | } 167 | 168 | /** 169 | * Determine if the customer's subscription has an incomplete payment. 170 | * 171 | * @param string $type 172 | * @return bool 173 | */ 174 | public function hasIncompletePayment($type = 'default') 175 | { 176 | if ($subscription = $this->subscription($type)) { 177 | return $subscription->hasIncompletePayment(); 178 | } 179 | 180 | return false; 181 | } 182 | 183 | /** 184 | * Determine if the Stripe model is actively subscribed to one of the given products. 185 | * 186 | * @param string|string[] $products 187 | * @param string $type 188 | * @return bool 189 | */ 190 | public function subscribedToProduct($products, $type = 'default') 191 | { 192 | $subscription = $this->subscription($type); 193 | 194 | if (! $subscription || ! $subscription->valid()) { 195 | return false; 196 | } 197 | 198 | foreach ((array) $products as $product) { 199 | if ($subscription->hasProduct($product)) { 200 | return true; 201 | } 202 | } 203 | 204 | return false; 205 | } 206 | 207 | /** 208 | * Determine if the Stripe model is actively subscribed to one of the given prices. 209 | * 210 | * @param string|string[] $prices 211 | * @param string $type 212 | * @return bool 213 | */ 214 | public function subscribedToPrice($prices, $type = 'default') 215 | { 216 | $subscription = $this->subscription($type); 217 | 218 | if (! $subscription || ! $subscription->valid()) { 219 | return false; 220 | } 221 | 222 | foreach ((array) $prices as $price) { 223 | if ($subscription->hasPrice($price)) { 224 | return true; 225 | } 226 | } 227 | 228 | return false; 229 | } 230 | 231 | /** 232 | * Determine if the customer has a valid subscription on the given product. 233 | * 234 | * @param string $product 235 | * @return bool 236 | */ 237 | public function onProduct($product) 238 | { 239 | return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($product) { 240 | return $subscription->valid() && $subscription->hasProduct($product); 241 | })); 242 | } 243 | 244 | /** 245 | * Determine if the customer has a valid subscription on the given price. 246 | * 247 | * @param string $price 248 | * @return bool 249 | */ 250 | public function onPrice($price) 251 | { 252 | return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($price) { 253 | return $subscription->valid() && $subscription->hasPrice($price); 254 | })); 255 | } 256 | 257 | /** 258 | * Get the tax rates to apply to the subscription. 259 | * 260 | * @return array 261 | */ 262 | public function taxRates() 263 | { 264 | return []; 265 | } 266 | 267 | /** 268 | * Get the tax rates to apply to individual subscription items. 269 | * 270 | * @return array 271 | */ 272 | public function priceTaxRates() 273 | { 274 | return []; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/Concerns/ManagesUsageBilling.php: -------------------------------------------------------------------------------- 1 | stripe()->billing->meters->all($options, $requestOptions)->data); 20 | } 21 | 22 | /** 23 | * Report usage for a metered product. 24 | * 25 | * @param string $meter 26 | * @param int $quantity 27 | * @param string|null $price 28 | * @param array $options 29 | * @param array $requestOptions 30 | * @return \Stripe\Billing\MeterEvent 31 | */ 32 | public function reportMeterEvent( 33 | string $meter, 34 | int $quantity = 1, 35 | array $options = [], 36 | array $requestOptions = [] 37 | ): MeterEvent { 38 | $this->assertCustomerExists(); 39 | 40 | return $this->stripe()->billing->meterEvents->create([ 41 | 'event_name' => $meter, 42 | 'payload' => [ 43 | 'stripe_customer_id' => $this->stripeId(), 44 | 'value' => $quantity, 45 | ], 46 | ...$options, 47 | ], $requestOptions); 48 | } 49 | 50 | /** 51 | * Get the usage records for a meter using its ID. 52 | * 53 | * @param string $meterId 54 | * @param array $options 55 | * @param array $requestOptions 56 | * @return \Illuminate\Support\Collection 57 | */ 58 | public function meterEventSummaries(string $meterId, int $startTime = 1, ?int $endTime = null, array $options = [], array $requestOptions = []): Collection 59 | { 60 | $this->assertCustomerExists(); 61 | 62 | if (! isset($endTime)) { 63 | $endTime = time(); 64 | } 65 | 66 | return new Collection($this->stripe()->billing->meters->allEventSummaries( 67 | $meterId, 68 | [ 69 | 'customer' => $this->stripeId(), 70 | 'start_time' => $startTime, 71 | 'end_time' => $endTime, 72 | ...$options, 73 | ], 74 | $requestOptions 75 | )->data); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Concerns/PerformsCharges.php: -------------------------------------------------------------------------------- 1 | 'automatic', 27 | 'confirm' => true, 28 | ], $options); 29 | 30 | $options['payment_method'] = $paymentMethod; 31 | 32 | $payment = $this->createPayment($amount, $options); 33 | 34 | $payment->validate(); 35 | 36 | return $payment; 37 | } 38 | 39 | /** 40 | * Create a new PaymentIntent instance. 41 | * 42 | * @param int $amount 43 | * @param array $options 44 | * @return \Laravel\Cashier\Payment 45 | */ 46 | public function pay($amount, array $options = []) 47 | { 48 | $options['automatic_payment_methods'] = ['enabled' => true]; 49 | 50 | unset($options['payment_method_types']); 51 | 52 | return $this->createPayment($amount, $options); 53 | } 54 | 55 | /** 56 | * Create a new PaymentIntent instance for the given payment method types. 57 | * 58 | * @param int $amount 59 | * @param array $paymentMethods 60 | * @param array $options 61 | * @return \Laravel\Cashier\Payment 62 | */ 63 | public function payWith($amount, array $paymentMethods, array $options = []) 64 | { 65 | $options['payment_method_types'] = $paymentMethods; 66 | 67 | unset($options['automatic_payment_methods']); 68 | 69 | return $this->createPayment($amount, $options); 70 | } 71 | 72 | /** 73 | * Create a new Payment instance with a Stripe PaymentIntent. 74 | * 75 | * @param int $amount 76 | * @param array $options 77 | * @return \Laravel\Cashier\Payment 78 | */ 79 | public function createPayment($amount, array $options = []) 80 | { 81 | $options = array_merge([ 82 | 'currency' => $this->preferredCurrency(), 83 | ], $options); 84 | 85 | $options['amount'] = $amount; 86 | 87 | if ($this->hasStripeId()) { 88 | $options['customer'] = $this->stripe_id; 89 | } 90 | 91 | return new Payment( 92 | static::stripe()->paymentIntents->create($options) 93 | ); 94 | } 95 | 96 | /** 97 | * Find a payment intent by ID. 98 | * 99 | * @param string $id 100 | * @return \Laravel\Cashier\Payment|null 101 | */ 102 | public function findPayment($id) 103 | { 104 | $stripePaymentIntent = null; 105 | 106 | try { 107 | $stripePaymentIntent = static::stripe()->paymentIntents->retrieve($id); 108 | } catch (StripeInvalidRequestException $exception) { 109 | // 110 | } 111 | 112 | return $stripePaymentIntent ? new Payment($stripePaymentIntent) : null; 113 | } 114 | 115 | /** 116 | * Refund a customer for a charge. 117 | * 118 | * @param string $paymentIntent 119 | * @param array $options 120 | * @return \Stripe\Refund 121 | */ 122 | public function refund($paymentIntent, array $options = []) 123 | { 124 | return static::stripe()->refunds->create( 125 | ['payment_intent' => $paymentIntent] + $options 126 | ); 127 | } 128 | 129 | /** 130 | * Begin a new checkout session for existing prices. 131 | * 132 | * @param array|string $items 133 | * @param array $sessionOptions 134 | * @param array $customerOptions 135 | * @return \Laravel\Cashier\Checkout 136 | */ 137 | public function checkout($items, array $sessionOptions = [], array $customerOptions = []) 138 | { 139 | return Checkout::customer($this, $this)->create($items, $sessionOptions, $customerOptions); 140 | } 141 | 142 | /** 143 | * Begin a new checkout session for a "one-off" charge. 144 | * 145 | * @param int $amount 146 | * @param string $name 147 | * @param int $quantity 148 | * @param array $sessionOptions 149 | * @param array $customerOptions 150 | * @param array $productData 151 | * @return \Laravel\Cashier\Checkout 152 | */ 153 | public function checkoutCharge($amount, $name, $quantity = 1, array $sessionOptions = [], array $customerOptions = [], array $productData = []) 154 | { 155 | return $this->checkout([[ 156 | 'price_data' => [ 157 | 'currency' => $this->preferredCurrency(), 158 | 'product_data' => array_merge($productData, [ 159 | 'name' => $name, 160 | ]), 161 | 'unit_amount' => $amount, 162 | ], 163 | 'quantity' => $quantity, 164 | ]], $sessionOptions, $customerOptions); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Concerns/Prorates.php: -------------------------------------------------------------------------------- 1 | prorationBehavior = 'none'; 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * Indicate that the price change should be prorated. 28 | * 29 | * @return $this 30 | */ 31 | public function prorate() 32 | { 33 | $this->prorationBehavior = 'create_prorations'; 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Indicate that the price change should always be invoiced. 40 | * 41 | * @return $this 42 | */ 43 | public function alwaysInvoice() 44 | { 45 | $this->prorationBehavior = 'always_invoice'; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Set the prorating behavior. 52 | * 53 | * @param string $prorationBehavior 54 | * @return $this 55 | */ 56 | public function setProrationBehavior($prorationBehavior) 57 | { 58 | $this->prorationBehavior = $prorationBehavior; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Determine the prorating behavior when updating the subscription. 65 | * 66 | * @return string 67 | */ 68 | public function prorateBehavior() 69 | { 70 | return $this->prorationBehavior; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Console/WebhookCommand.php: -------------------------------------------------------------------------------- 1 | webhookEndpoints; 48 | 49 | $endpoint = $webhookEndpoints->create(array_filter([ 50 | 'enabled_events' => config('cashier.webhook.events') ?: self::DEFAULT_EVENTS, 51 | 'url' => $this->option('url') ?? route('cashier.webhook'), 52 | 'api_version' => $this->option('api-version') ?? Cashier::STRIPE_VERSION, 53 | ])); 54 | 55 | $this->components->info('The Stripe webhook was created successfully. Retrieve the webhook secret in your Stripe dashboard and define it as an environment variable.'); 56 | 57 | if ($this->option('disabled')) { 58 | $webhookEndpoints->update($endpoint->id, ['disabled' => true]); 59 | 60 | $this->components->info('The Stripe webhook was disabled as requested. You may enable the webhook via the Stripe dashboard when needed.'); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Contracts/InvoiceRenderer.php: -------------------------------------------------------------------------------- 1 | coupon = $coupon; 28 | } 29 | 30 | /** 31 | * Get the readable name for the Coupon. 32 | * 33 | * @return string 34 | */ 35 | public function name() 36 | { 37 | return $this->coupon->name ?: $this->coupon->id; 38 | } 39 | 40 | /** 41 | * Determine if the coupon is a percentage. 42 | * 43 | * @return bool 44 | */ 45 | public function isPercentage() 46 | { 47 | return ! is_null($this->coupon->percent_off); 48 | } 49 | 50 | /** 51 | * Get the discount percentage for the invoice. 52 | * 53 | * @return float|null 54 | */ 55 | public function percentOff() 56 | { 57 | return $this->coupon->percent_off; 58 | } 59 | 60 | /** 61 | * Get the amount off for the coupon. 62 | * 63 | * @return string|null 64 | */ 65 | public function amountOff() 66 | { 67 | if (! is_null($this->coupon->amount_off)) { 68 | return $this->formatAmount($this->rawAmountOff()); 69 | } 70 | } 71 | 72 | /** 73 | * Get the raw amount off for the coupon. 74 | * 75 | * @return int|null 76 | */ 77 | public function rawAmountOff() 78 | { 79 | return $this->coupon->amount_off; 80 | } 81 | 82 | /** 83 | * Format the given amount into a displayable currency. 84 | * 85 | * @param int $amount 86 | * @return string 87 | */ 88 | protected function formatAmount($amount) 89 | { 90 | return Cashier::formatAmount($amount, $this->coupon->currency); 91 | } 92 | 93 | /** 94 | * Get the Stripe Coupon instance. 95 | * 96 | * @return \Stripe\Coupon 97 | */ 98 | public function asStripeCoupon() 99 | { 100 | return $this->coupon; 101 | } 102 | 103 | /** 104 | * Get the instance as an array. 105 | * 106 | * @return array 107 | */ 108 | public function toArray() 109 | { 110 | return $this->asStripeCoupon()->toArray(); 111 | } 112 | 113 | /** 114 | * Convert the object to its JSON representation. 115 | * 116 | * @param int $options 117 | * @return string 118 | */ 119 | public function toJson($options = 0) 120 | { 121 | return json_encode($this->jsonSerialize(), $options); 122 | } 123 | 124 | /** 125 | * Convert the object into something JSON serializable. 126 | * 127 | * @return array 128 | */ 129 | #[\ReturnTypeWillChange] 130 | public function jsonSerialize() 131 | { 132 | return $this->toArray(); 133 | } 134 | 135 | /** 136 | * Dynamically get values from the Stripe object. 137 | * 138 | * @param string $key 139 | * @return mixed 140 | */ 141 | public function __get($key) 142 | { 143 | return $this->coupon->{$key}; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/CustomerBalanceTransaction.php: -------------------------------------------------------------------------------- 1 | stripe_id !== $transaction->customer) { 36 | throw InvalidCustomerBalanceTransaction::invalidOwner($transaction, $owner); 37 | } 38 | 39 | $this->owner = $owner; 40 | $this->transaction = $transaction; 41 | } 42 | 43 | /** 44 | * Get the total transaction amount. 45 | * 46 | * @return string 47 | */ 48 | public function amount() 49 | { 50 | return $this->formatAmount($this->rawAmount()); 51 | } 52 | 53 | /** 54 | * Get the raw total transaction amount. 55 | * 56 | * @return int 57 | */ 58 | public function rawAmount() 59 | { 60 | return $this->transaction->amount; 61 | } 62 | 63 | /** 64 | * Get the ending balance. 65 | * 66 | * @return string 67 | */ 68 | public function endingBalance() 69 | { 70 | return $this->formatAmount($this->rawEndingBalance()); 71 | } 72 | 73 | /** 74 | * Get the raw ending balance. 75 | * 76 | * @return int 77 | */ 78 | public function rawEndingBalance() 79 | { 80 | return $this->transaction->ending_balance; 81 | } 82 | 83 | /** 84 | * Format the given amount into a displayable currency. 85 | * 86 | * @param int $amount 87 | * @return string 88 | */ 89 | protected function formatAmount($amount) 90 | { 91 | return Cashier::formatAmount($amount, $this->transaction->currency); 92 | } 93 | 94 | /** 95 | * Return the related invoice for this transaction. 96 | * 97 | * @return \Laravel\Cashier\Invoice 98 | */ 99 | public function invoice() 100 | { 101 | return $this->transaction->invoice 102 | ? $this->owner->findInvoice($this->transaction->invoice) 103 | : null; 104 | } 105 | 106 | /** 107 | * Get the Stripe CustomerBalanceTransaction instance. 108 | * 109 | * @return \Stripe\CustomerBalanceTransaction 110 | */ 111 | public function asStripeCustomerBalanceTransaction() 112 | { 113 | return $this->transaction; 114 | } 115 | 116 | /** 117 | * Get the instance as an array. 118 | * 119 | * @return array 120 | */ 121 | public function toArray() 122 | { 123 | return $this->asStripeCustomerBalanceTransaction()->toArray(); 124 | } 125 | 126 | /** 127 | * Convert the object to its JSON representation. 128 | * 129 | * @param int $options 130 | * @return string 131 | */ 132 | public function toJson($options = 0) 133 | { 134 | return json_encode($this->jsonSerialize(), $options); 135 | } 136 | 137 | /** 138 | * Convert the object into something JSON serializable. 139 | * 140 | * @return array 141 | */ 142 | #[\ReturnTypeWillChange] 143 | public function jsonSerialize() 144 | { 145 | return $this->toArray(); 146 | } 147 | 148 | /** 149 | * Dynamically get values from the Stripe object. 150 | * 151 | * @param string $key 152 | * @return mixed 153 | */ 154 | public function __get($key) 155 | { 156 | return $this->transaction->{$key}; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Discount.php: -------------------------------------------------------------------------------- 1 | discount = $discount; 29 | } 30 | 31 | /** 32 | * Get the coupon applied to the discount. 33 | * 34 | * @return \Laravel\Cashier\Coupon 35 | */ 36 | public function coupon() 37 | { 38 | return new Coupon($this->discount->coupon); 39 | } 40 | 41 | /** 42 | * Get the promotion code applied to create this discount. 43 | * 44 | * @return \Laravel\Cashier\PromotionCode|null 45 | */ 46 | public function promotionCode() 47 | { 48 | if (! is_null($this->discount->promotion_code) && ! is_string($this->discount->promotion_code)) { 49 | return new PromotionCode($this->discount->promotion_code); 50 | } 51 | } 52 | 53 | /** 54 | * Get the date that the coupon was applied. 55 | * 56 | * @return \Carbon\Carbon 57 | */ 58 | public function start() 59 | { 60 | return Carbon::createFromTimestamp($this->discount->start); 61 | } 62 | 63 | /** 64 | * Get the date that this discount will end. 65 | * 66 | * @return \Carbon\Carbon|null 67 | */ 68 | public function end() 69 | { 70 | if (! is_null($this->discount->end)) { 71 | return Carbon::createFromTimestamp($this->discount->end); 72 | } 73 | } 74 | 75 | /** 76 | * Get the Stripe Discount instance. 77 | * 78 | * @return \Stripe\Discount 79 | */ 80 | public function asStripeDiscount() 81 | { 82 | return $this->discount; 83 | } 84 | 85 | /** 86 | * Get the instance as an array. 87 | * 88 | * @return array 89 | */ 90 | public function toArray() 91 | { 92 | return $this->asStripeDiscount()->toArray(); 93 | } 94 | 95 | /** 96 | * Convert the object to its JSON representation. 97 | * 98 | * @param int $options 99 | * @return string 100 | */ 101 | public function toJson($options = 0) 102 | { 103 | return json_encode($this->jsonSerialize(), $options); 104 | } 105 | 106 | /** 107 | * Convert the object into something JSON serializable. 108 | * 109 | * @return array 110 | */ 111 | #[\ReturnTypeWillChange] 112 | public function jsonSerialize() 113 | { 114 | return $this->toArray(); 115 | } 116 | 117 | /** 118 | * Dynamically get values from the Stripe object. 119 | * 120 | * @param string $key 121 | * @return mixed 122 | */ 123 | public function __get($key) 124 | { 125 | return $this->discount->{$key}; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Events/WebhookHandled.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Events/WebhookReceived.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exceptions/CustomerAlreadyCreated.php: -------------------------------------------------------------------------------- 1 | stripe_id}."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/IncompletePayment.php: -------------------------------------------------------------------------------- 1 | payment = $payment; 32 | } 33 | 34 | /** 35 | * Create a new IncompletePayment instance with a `payment_action_required` type. 36 | * 37 | * @param \Laravel\Cashier\Payment $payment 38 | * @return static 39 | */ 40 | public static function paymentMethodRequired(Payment $payment) 41 | { 42 | return new static( 43 | $payment, 44 | 'The payment attempt failed because of an invalid payment method.' 45 | ); 46 | } 47 | 48 | /** 49 | * Create a new IncompletePayment instance with a `requires_action` type. 50 | * 51 | * @param \Laravel\Cashier\Payment $payment 52 | * @return static 53 | */ 54 | public static function requiresAction(Payment $payment) 55 | { 56 | return new static( 57 | $payment, 58 | 'The payment attempt failed because additional action is required before it can be completed.' 59 | ); 60 | } 61 | 62 | /** 63 | * Create a new IncompletePayment instance with a `requires_confirmation` type. 64 | * 65 | * @param \Laravel\Cashier\Payment $payment 66 | * @return static 67 | */ 68 | public static function requiresConfirmation(Payment $payment) 69 | { 70 | return new static( 71 | $payment, 72 | 'The payment attempt failed because it needs to be confirmed before it can be completed.' 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidCustomer.php: -------------------------------------------------------------------------------- 1 | id}` does not belong to customer `$owner->stripe_id`."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidInvoice.php: -------------------------------------------------------------------------------- 1 | id}` does not belong to this customer `$owner->stripe_id`."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPaymentMethod.php: -------------------------------------------------------------------------------- 1 | id}`'s customer `{$paymentMethod->customer}` does not belong to this customer `$owner->stripe_id`." 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exceptions/SubscriptionUpdateFailure.php: -------------------------------------------------------------------------------- 1 | stripe_id}\" cannot be updated because its payment is incomplete." 20 | ); 21 | } 22 | 23 | /** 24 | * Create a new SubscriptionUpdateFailure instance. 25 | * 26 | * @param \Laravel\Cashier\Subscription $subscription 27 | * @param string $price 28 | * @return static 29 | */ 30 | public static function duplicatePrice(Subscription $subscription, $price) 31 | { 32 | return new static( 33 | "The price \"$price\" is already attached to subscription \"{$subscription->stripe_id}\"." 34 | ); 35 | } 36 | 37 | /** 38 | * Create a new SubscriptionUpdateFailure instance. 39 | * 40 | * @param \Laravel\Cashier\Subscription $subscription 41 | * @return static 42 | */ 43 | public static function cannotDeleteLastPrice(Subscription $subscription) 44 | { 45 | return new static( 46 | "The price on subscription \"{$subscription->stripe_id}\" cannot be removed because it is the last one." 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Controllers/PaymentController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifyRedirectUrl::class); 22 | } 23 | 24 | /** 25 | * Display the form to gather additional payment verification for the given payment. 26 | * 27 | * @param string $id 28 | * @return \Illuminate\Contracts\View\View 29 | */ 30 | public function show($id) 31 | { 32 | try { 33 | $payment = new Payment(Cashier::stripe()->paymentIntents->retrieve( 34 | $id, ['expand' => ['payment_method']]) 35 | ); 36 | } catch (StripeInvalidRequestException $exception) { 37 | abort(404, 'Payment not found.'); 38 | } 39 | 40 | $paymentIntent = Arr::only($payment->asStripePaymentIntent()->toArray(), [ 41 | 'id', 'status', 'payment_method_types', 'client_secret', 'payment_method', 42 | ]); 43 | 44 | $paymentIntent['payment_method'] = Arr::only($paymentIntent['payment_method'] ?? [], 'id'); 45 | 46 | return view('cashier::payment', [ 47 | 'stripeKey' => config('cashier.key'), 48 | 'amount' => $payment->amount(), 49 | 'payment' => $payment, 50 | 'paymentIntent' => array_filter($paymentIntent), 51 | 'paymentMethod' => (string) request('source_type', optional($payment->payment_method)->type), 52 | 'errorMessage' => request('redirect_status') === 'failed' 53 | ? 'Something went wrong when trying to confirm the payment. Please try again.' 54 | : '', 55 | 'customer' => $payment->customer(), 56 | 'redirect' => url(request('redirect', '/')), 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Http/Controllers/WebhookController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifyWebhookSignature::class); 31 | } 32 | } 33 | 34 | /** 35 | * Handle a Stripe webhook call. 36 | * 37 | * @param \Illuminate\Http\Request $request 38 | * @return \Symfony\Component\HttpFoundation\Response 39 | */ 40 | public function handleWebhook(Request $request) 41 | { 42 | $payload = json_decode($request->getContent(), true); 43 | $method = 'handle'.Str::studly(str_replace('.', '_', $payload['type'])); 44 | 45 | WebhookReceived::dispatch($payload); 46 | 47 | if (method_exists($this, $method)) { 48 | $this->setMaxNetworkRetries(); 49 | 50 | $response = $this->{$method}($payload); 51 | 52 | WebhookHandled::dispatch($payload); 53 | 54 | return $response; 55 | } 56 | 57 | return $this->missingMethod($payload); 58 | } 59 | 60 | /** 61 | * Handle customer subscription created. 62 | * 63 | * @param array $payload 64 | * @return \Symfony\Component\HttpFoundation\Response 65 | */ 66 | protected function handleCustomerSubscriptionCreated(array $payload) 67 | { 68 | $user = $this->getUserByStripeId($payload['data']['object']['customer']); 69 | 70 | if ($user) { 71 | $data = $payload['data']['object']; 72 | 73 | if (! $user->subscriptions->contains('stripe_id', $data['id'])) { 74 | if (isset($data['trial_end'])) { 75 | $trialEndsAt = Carbon::createFromTimestamp($data['trial_end']); 76 | } else { 77 | $trialEndsAt = null; 78 | } 79 | 80 | $firstItem = $data['items']['data'][0]; 81 | $isSinglePrice = count($data['items']['data']) === 1; 82 | 83 | $subscription = $user->subscriptions()->create([ 84 | 'type' => $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload), 85 | 'stripe_id' => $data['id'], 86 | 'stripe_status' => $data['status'], 87 | 'stripe_price' => $isSinglePrice ? $firstItem['price']['id'] : null, 88 | 'quantity' => $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null, 89 | 'trial_ends_at' => $trialEndsAt, 90 | 'ends_at' => null, 91 | ]); 92 | 93 | foreach ($data['items']['data'] as $item) { 94 | $subscription->items()->create([ 95 | 'stripe_id' => $item['id'], 96 | 'stripe_product' => $item['price']['product'], 97 | 'stripe_price' => $item['price']['id'], 98 | 'quantity' => $item['quantity'] ?? null, 99 | ]); 100 | } 101 | } 102 | 103 | // Terminate the billable's generic trial if it exists... 104 | if (! is_null($user->trial_ends_at)) { 105 | $user->trial_ends_at = null; 106 | $user->save(); 107 | } 108 | } 109 | 110 | return $this->successMethod(); 111 | } 112 | 113 | /** 114 | * Determines the type that should be used when new subscriptions are created from the Stripe dashboard. 115 | * 116 | * @param array $payload 117 | * @return string 118 | */ 119 | protected function newSubscriptionType(array $payload) 120 | { 121 | return 'default'; 122 | } 123 | 124 | /** 125 | * Handle customer subscription updated. 126 | * 127 | * @param array $payload 128 | * @return \Symfony\Component\HttpFoundation\Response 129 | */ 130 | protected function handleCustomerSubscriptionUpdated(array $payload) 131 | { 132 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 133 | $data = $payload['data']['object']; 134 | 135 | $subscription = $user->subscriptions()->firstOrNew(['stripe_id' => $data['id']]); 136 | 137 | if ( 138 | isset($data['status']) && 139 | $data['status'] === StripeSubscription::STATUS_INCOMPLETE_EXPIRED 140 | ) { 141 | $subscription->items()->delete(); 142 | $subscription->delete(); 143 | 144 | return; 145 | } 146 | 147 | $subscription->type = $subscription->type ?? $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload); 148 | 149 | $firstItem = $data['items']['data'][0]; 150 | $isSinglePrice = count($data['items']['data']) === 1; 151 | 152 | // Price... 153 | $subscription->stripe_price = $isSinglePrice ? $firstItem['price']['id'] : null; 154 | 155 | // Quantity... 156 | $subscription->quantity = $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null; 157 | 158 | // Trial ending date... 159 | if (isset($data['trial_end'])) { 160 | $trialEnd = Carbon::createFromTimestamp($data['trial_end']); 161 | 162 | if (! $subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) { 163 | $subscription->trial_ends_at = $trialEnd; 164 | } 165 | } 166 | 167 | // Cancellation date... 168 | if ($data['cancel_at_period_end'] ?? false) { 169 | $subscription->ends_at = $subscription->onTrial() 170 | ? $subscription->trial_ends_at 171 | : Carbon::createFromTimestamp($data['current_period_end']); 172 | } elseif (isset($data['cancel_at']) || isset($data['canceled_at'])) { 173 | $subscription->ends_at = Carbon::createFromTimestamp($data['cancel_at'] ?? $data['canceled_at']); 174 | } else { 175 | $subscription->ends_at = null; 176 | } 177 | 178 | // Status... 179 | if (isset($data['status'])) { 180 | $subscription->stripe_status = $data['status']; 181 | } 182 | 183 | $subscription->save(); 184 | 185 | // Update subscription items... 186 | if (isset($data['items'])) { 187 | $subscriptionItemIds = []; 188 | 189 | foreach ($data['items']['data'] as $item) { 190 | $subscriptionItemIds[] = $item['id']; 191 | 192 | $subscription->items()->updateOrCreate([ 193 | 'stripe_id' => $item['id'], 194 | ], [ 195 | 'stripe_product' => $item['price']['product'], 196 | 'stripe_price' => $item['price']['id'], 197 | 'quantity' => $item['quantity'] ?? null, 198 | ]); 199 | } 200 | 201 | // Delete items that aren't attached to the subscription anymore... 202 | $subscription->items()->whereNotIn('stripe_id', $subscriptionItemIds)->delete(); 203 | } 204 | } 205 | 206 | return $this->successMethod(); 207 | } 208 | 209 | /** 210 | * Handle the cancellation of a customer subscription. 211 | * 212 | * @param array $payload 213 | * @return \Symfony\Component\HttpFoundation\Response 214 | */ 215 | protected function handleCustomerSubscriptionDeleted(array $payload) 216 | { 217 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 218 | $user->subscriptions->filter(function ($subscription) use ($payload) { 219 | return $subscription->stripe_id === $payload['data']['object']['id']; 220 | })->each(function ($subscription) { 221 | $subscription->skipTrial()->markAsCanceled(); 222 | }); 223 | } 224 | 225 | return $this->successMethod(); 226 | } 227 | 228 | /** 229 | * Handle customer updated. 230 | * 231 | * @param array $payload 232 | * @return \Symfony\Component\HttpFoundation\Response 233 | */ 234 | protected function handleCustomerUpdated(array $payload) 235 | { 236 | if ($user = $this->getUserByStripeId($payload['data']['object']['id'])) { 237 | $user->updateDefaultPaymentMethodFromStripe(); 238 | } 239 | 240 | return $this->successMethod(); 241 | } 242 | 243 | /** 244 | * Handle deleted customer. 245 | * 246 | * @param array $payload 247 | * @return \Symfony\Component\HttpFoundation\Response 248 | */ 249 | protected function handleCustomerDeleted(array $payload) 250 | { 251 | if ($user = $this->getUserByStripeId($payload['data']['object']['id'])) { 252 | $user->subscriptions->each(function (Subscription $subscription) { 253 | $subscription->skipTrial()->markAsCanceled(); 254 | }); 255 | 256 | $user->forceFill([ 257 | 'stripe_id' => null, 258 | 'trial_ends_at' => null, 259 | 'pm_type' => null, 260 | 'pm_last_four' => null, 261 | ])->save(); 262 | } 263 | 264 | return $this->successMethod(); 265 | } 266 | 267 | /** 268 | * Handle payment method automatically updated by vendor. 269 | * 270 | * @param array $payload 271 | * @return \Symfony\Component\HttpFoundation\Response 272 | */ 273 | protected function handlePaymentMethodAutomaticallyUpdated(array $payload) 274 | { 275 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 276 | $user->updateDefaultPaymentMethodFromStripe(); 277 | } 278 | 279 | return $this->successMethod(); 280 | } 281 | 282 | /** 283 | * Handle payment action required for invoice. 284 | * 285 | * @param array $payload 286 | * @return \Symfony\Component\HttpFoundation\Response 287 | */ 288 | protected function handleInvoicePaymentActionRequired(array $payload) 289 | { 290 | if (is_null($notification = config('cashier.payment_notification'))) { 291 | return $this->successMethod(); 292 | } 293 | 294 | if ($payload['data']['object']['metadata']['is_on_session_checkout'] ?? false) { 295 | return $this->successMethod(); 296 | } 297 | 298 | if ($payload['data']['object']['subscription_details']['metadata']['is_on_session_checkout'] ?? false) { 299 | return $this->successMethod(); 300 | } 301 | 302 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 303 | if (in_array(Notifiable::class, class_uses_recursive($user))) { 304 | $payment = new Payment($user->stripe()->paymentIntents->retrieve( 305 | $payload['data']['object']['payment_intent'] 306 | )); 307 | 308 | $user->notify(new $notification($payment)); 309 | } 310 | } 311 | 312 | return $this->successMethod(); 313 | } 314 | 315 | /** 316 | * Get the customer instance by Stripe ID. 317 | * 318 | * @param string|null $stripeId 319 | * @return \Laravel\Cashier\Billable|null 320 | */ 321 | protected function getUserByStripeId($stripeId) 322 | { 323 | return Cashier::findBillable($stripeId); 324 | } 325 | 326 | /** 327 | * Handle successful calls on the controller. 328 | * 329 | * @param array $parameters 330 | * @return \Symfony\Component\HttpFoundation\Response 331 | */ 332 | protected function successMethod($parameters = []) 333 | { 334 | return new Response('Webhook Handled', 200); 335 | } 336 | 337 | /** 338 | * Handle calls to missing methods on the controller. 339 | * 340 | * @param array $parameters 341 | * @return \Symfony\Component\HttpFoundation\Response 342 | */ 343 | protected function missingMethod($parameters = []) 344 | { 345 | return new Response; 346 | } 347 | 348 | /** 349 | * Set the number of automatic retries due to an object lock timeout from Stripe. 350 | * 351 | * @param int $retries 352 | * @return void 353 | */ 354 | protected function setMaxNetworkRetries($retries = 3) 355 | { 356 | Stripe::setMaxNetworkRetries($retries); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifyRedirectUrl.php: -------------------------------------------------------------------------------- 1 | get('redirect')) { 22 | return $next($request); 23 | } 24 | 25 | $url = parse_url($redirect); 26 | 27 | if (isset($url['host']) && $url['host'] !== $request->getHost()) { 28 | throw new AccessDeniedHttpException('Redirect host mismatch.'); 29 | } 30 | 31 | return $next($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifyWebhookSignature.php: -------------------------------------------------------------------------------- 1 | getContent(), 26 | $request->header('Stripe-Signature'), 27 | config('cashier.webhook.secret'), 28 | config('cashier.webhook.tolerance') 29 | ); 30 | } catch (SignatureVerificationException $exception) { 31 | throw new AccessDeniedHttpException($exception->getMessage(), $exception); 32 | } 33 | 34 | return $next($request); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/InvoiceLineItem.php: -------------------------------------------------------------------------------- 1 | invoice = $invoice; 39 | $this->item = $item; 40 | } 41 | 42 | /** 43 | * Get the total for the invoice line item. 44 | * 45 | * @return string 46 | */ 47 | public function total() 48 | { 49 | return $this->formatAmount($this->item->amount); 50 | } 51 | 52 | /** 53 | * Get the unit amount excluding tax for the invoice line item. 54 | * 55 | * @return string 56 | */ 57 | public function unitAmountExcludingTax() 58 | { 59 | return $this->formatAmount($this->item->unit_amount_excluding_tax ?? 0); 60 | } 61 | 62 | /** 63 | * Determine if the line item has both inclusive and exclusive tax. 64 | * 65 | * @return bool 66 | */ 67 | public function hasBothInclusiveAndExclusiveTax() 68 | { 69 | return $this->inclusiveTaxPercentage() && $this->exclusiveTaxPercentage(); 70 | } 71 | 72 | /** 73 | * Get the total percentage of the default inclusive tax for the invoice line item. 74 | * 75 | * @return float|int|null 76 | */ 77 | public function inclusiveTaxPercentage() 78 | { 79 | if ($this->invoice->isNotTaxExempt()) { 80 | return $this->calculateTaxPercentageByTaxAmount(true); 81 | } 82 | 83 | return $this->calculateTaxPercentageByTaxRate(true); 84 | } 85 | 86 | /** 87 | * Get the total percentage of the default exclusive tax for the invoice line item. 88 | * 89 | * @return float|int 90 | */ 91 | public function exclusiveTaxPercentage() 92 | { 93 | if ($this->invoice->isNotTaxExempt()) { 94 | return $this->calculateTaxPercentageByTaxAmount(false); 95 | } 96 | 97 | return $this->calculateTaxPercentageByTaxRate(false); 98 | } 99 | 100 | /** 101 | * Calculate the total tax percentage for either the inclusive or exclusive tax by tax rate. 102 | * 103 | * @param bool $inclusive 104 | * @return float|int 105 | */ 106 | protected function calculateTaxPercentageByTaxRate($inclusive) 107 | { 108 | if (! $this->item->tax_rates) { 109 | return 0; 110 | } 111 | 112 | return Collection::make($this->item->tax_rates) 113 | ->filter(function (StripeTaxRate $taxRate) use ($inclusive) { 114 | return $taxRate->inclusive === (bool) $inclusive; 115 | }) 116 | ->sum(function (StripeTaxRate $taxRate) { 117 | return $taxRate->percentage; 118 | }); 119 | } 120 | 121 | /** 122 | * Calculate the total tax percentage for either the inclusive or exclusive tax by tax amount. 123 | * 124 | * @param bool $inclusive 125 | * @return float|int 126 | */ 127 | protected function calculateTaxPercentageByTaxAmount($inclusive) 128 | { 129 | if (! $this->item->tax_amounts) { 130 | return 0; 131 | } 132 | 133 | return Collection::make($this->item->tax_amounts) 134 | ->filter(function (object $taxAmount) use ($inclusive) { 135 | return $taxAmount->inclusive === (bool) $inclusive; 136 | }) 137 | ->sum(function (object $taxAmount) { 138 | return $taxAmount->tax_rate->percentage; 139 | }); 140 | } 141 | 142 | /** 143 | * Determine if the invoice line item has tax rates. 144 | * 145 | * @return bool 146 | */ 147 | public function hasTaxRates() 148 | { 149 | if ($this->invoice->isNotTaxExempt()) { 150 | return ! empty($this->item->tax_amounts); 151 | } 152 | 153 | return ! empty($this->item->tax_rates); 154 | } 155 | 156 | /** 157 | * Get a human readable date for the start date. 158 | * 159 | * @return string|null 160 | */ 161 | public function startDate() 162 | { 163 | if ($this->hasPeriod()) { 164 | return $this->startDateAsCarbon()->toFormattedDateString(); 165 | } 166 | } 167 | 168 | /** 169 | * Get a human readable date for the end date. 170 | * 171 | * @return string|null 172 | */ 173 | public function endDate() 174 | { 175 | if ($this->hasPeriod()) { 176 | return $this->endDateAsCarbon()->toFormattedDateString(); 177 | } 178 | } 179 | 180 | /** 181 | * Get a Carbon instance for the start date. 182 | * 183 | * @return \Carbon\Carbon|null 184 | */ 185 | public function startDateAsCarbon() 186 | { 187 | if ($this->hasPeriod()) { 188 | return Carbon::createFromTimestampUTC($this->item->period->start); 189 | } 190 | } 191 | 192 | /** 193 | * Get a Carbon instance for the end date. 194 | * 195 | * @return \Carbon\Carbon|null 196 | */ 197 | public function endDateAsCarbon() 198 | { 199 | if ($this->hasPeriod()) { 200 | return Carbon::createFromTimestampUTC($this->item->period->end); 201 | } 202 | } 203 | 204 | /** 205 | * Determine if the invoice line item has a defined period. 206 | * 207 | * @return bool 208 | */ 209 | public function hasPeriod() 210 | { 211 | return ! is_null($this->item->period); 212 | } 213 | 214 | /** 215 | * Determine if the invoice line item has a period with the same start and end date. 216 | * 217 | * @return bool 218 | */ 219 | public function periodStartAndEndAreEqual() 220 | { 221 | return $this->hasPeriod() ? $this->item->period->start === $this->item->period->end : false; 222 | } 223 | 224 | /** 225 | * Determine if the invoice line item is for a subscription. 226 | * 227 | * @return bool 228 | */ 229 | public function isSubscription() 230 | { 231 | return $this->item->type === 'subscription'; 232 | } 233 | 234 | /** 235 | * Format the given amount into a displayable currency. 236 | * 237 | * @param int $amount 238 | * @return string 239 | */ 240 | protected function formatAmount($amount) 241 | { 242 | return Cashier::formatAmount($amount, $this->item->currency); 243 | } 244 | 245 | /** 246 | * Get the Stripe model instance. 247 | * 248 | * @return \Laravel\Cashier\Invoice 249 | */ 250 | public function invoice() 251 | { 252 | return $this->invoice; 253 | } 254 | 255 | /** 256 | * Get the underlying Stripe invoice line item. 257 | * 258 | * @return \Stripe\InvoiceLineItem 259 | */ 260 | public function asStripeInvoiceLineItem() 261 | { 262 | return $this->item; 263 | } 264 | 265 | /** 266 | * Get the instance as an array. 267 | * 268 | * @return array 269 | */ 270 | public function toArray() 271 | { 272 | return $this->asStripeInvoiceLineItem()->toArray(); 273 | } 274 | 275 | /** 276 | * Convert the object to its JSON representation. 277 | * 278 | * @param int $options 279 | * @return string 280 | */ 281 | public function toJson($options = 0) 282 | { 283 | return json_encode($this->jsonSerialize(), $options); 284 | } 285 | 286 | /** 287 | * Convert the object into something JSON serializable. 288 | * 289 | * @return array 290 | */ 291 | #[\ReturnTypeWillChange] 292 | public function jsonSerialize() 293 | { 294 | return $this->toArray(); 295 | } 296 | 297 | /** 298 | * Dynamically access the Stripe invoice line item instance. 299 | * 300 | * @param string $key 301 | * @return mixed 302 | */ 303 | public function __get($key) 304 | { 305 | return $this->item->{$key}; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/Invoices/DompdfInvoiceRenderer.php: -------------------------------------------------------------------------------- 1 | setIsRemoteEnabled($options['remote_enabled'] ?? false); 23 | $dompdfOptions->setChroot(base_path()); 24 | 25 | $dompdf = new Dompdf($dompdfOptions); 26 | $dompdf->setPaper($options['paper'] ?? 'letter'); 27 | $dompdf->loadHtml($invoice->view($data)->render()); 28 | $dompdf->render(); 29 | 30 | return (string) $dompdf->output(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Jobs/SyncCustomerDetails.php: -------------------------------------------------------------------------------- 1 | billable = $billable; 34 | } 35 | 36 | /** 37 | * Execute the job. 38 | * 39 | * @return void 40 | */ 41 | public function handle() 42 | { 43 | $this->billable->syncStripeCustomerDetails(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function error($message, array $context = []) 32 | { 33 | $this->logger->error($message, $context); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Notifications/ConfirmPayment.php: -------------------------------------------------------------------------------- 1 | paymentId = $payment->id; 38 | $this->amount = $payment->amount(); 39 | } 40 | 41 | /** 42 | * Get the notification's delivery channels. 43 | * 44 | * @param mixed $notifiable 45 | * @return array 46 | */ 47 | public function via($notifiable) 48 | { 49 | return ['mail']; 50 | } 51 | 52 | /** 53 | * Get the mail representation of the notification. 54 | * 55 | * @param mixed $notifiable 56 | * @return \Illuminate\Notifications\Messages\MailMessage 57 | */ 58 | public function toMail($notifiable) 59 | { 60 | $url = route('cashier.payment', ['id' => $this->paymentId]); 61 | 62 | return (new MailMessage) 63 | ->subject(__('Confirm Payment')) 64 | ->greeting(__('Confirm your :amount payment', ['amount' => $this->amount])) 65 | ->line(__('Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.')) 66 | ->action(__('Confirm Payment'), $url); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Payment.php: -------------------------------------------------------------------------------- 1 | paymentIntent = $paymentIntent; 39 | } 40 | 41 | /** 42 | * Get the total amount that will be paid. 43 | * 44 | * @return string 45 | */ 46 | public function amount() 47 | { 48 | return Cashier::formatAmount($this->rawAmount(), $this->paymentIntent->currency); 49 | } 50 | 51 | /** 52 | * Get the raw total amount that will be paid. 53 | * 54 | * @return int 55 | */ 56 | public function rawAmount() 57 | { 58 | return $this->paymentIntent->amount; 59 | } 60 | 61 | /** 62 | * The Stripe PaymentIntent client secret. 63 | * 64 | * @return string 65 | */ 66 | public function clientSecret() 67 | { 68 | return $this->paymentIntent->client_secret; 69 | } 70 | 71 | /** 72 | * Capture a payment that is being held for the customer. 73 | * 74 | * @param array $options 75 | * @return \Stripe\PaymentIntent 76 | */ 77 | public function capture(array $options = []) 78 | { 79 | return $this->paymentIntent->capture($options); 80 | } 81 | 82 | /** 83 | * Determine if the payment needs a valid payment method. 84 | * 85 | * @return bool 86 | */ 87 | public function requiresPaymentMethod() 88 | { 89 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD; 90 | } 91 | 92 | /** 93 | * Determine if the payment needs an extra action like 3D Secure. 94 | * 95 | * @return bool 96 | */ 97 | public function requiresAction() 98 | { 99 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_ACTION; 100 | } 101 | 102 | /** 103 | * Determine if the payment needs to be confirmed. 104 | * 105 | * @return bool 106 | */ 107 | public function requiresConfirmation() 108 | { 109 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_CONFIRMATION; 110 | } 111 | 112 | /** 113 | * Determine if the payment needs to be captured. 114 | * 115 | * @return bool 116 | */ 117 | public function requiresCapture() 118 | { 119 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_CAPTURE; 120 | } 121 | 122 | /** 123 | * Cancel the payment. 124 | * 125 | * @param array $options 126 | * @return \Stripe\PaymentIntent 127 | */ 128 | public function cancel(array $options = []) 129 | { 130 | return $this->paymentIntent->cancel($options); 131 | } 132 | 133 | /** 134 | * Determine if the payment was canceled. 135 | * 136 | * @return bool 137 | */ 138 | public function isCanceled() 139 | { 140 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_CANCELED; 141 | } 142 | 143 | /** 144 | * Determine if the payment was successful. 145 | * 146 | * @return bool 147 | */ 148 | public function isSucceeded() 149 | { 150 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_SUCCEEDED; 151 | } 152 | 153 | /** 154 | * Determine if the payment is processing. 155 | * 156 | * @return bool 157 | */ 158 | public function isProcessing() 159 | { 160 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_PROCESSING; 161 | } 162 | 163 | /** 164 | * Validate if the payment intent was successful and throw an exception if not. 165 | * 166 | * @return void 167 | * 168 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 169 | */ 170 | public function validate() 171 | { 172 | if ($this->requiresPaymentMethod()) { 173 | throw IncompletePayment::paymentMethodRequired($this); 174 | } elseif ($this->requiresAction()) { 175 | throw IncompletePayment::requiresAction($this); 176 | } elseif ($this->requiresConfirmation()) { 177 | throw IncompletePayment::requiresConfirmation($this); 178 | } 179 | } 180 | 181 | /** 182 | * Retrieve the related customer for the payment intent if one exists. 183 | * 184 | * @return \Laravel\Cashier\Billable|null 185 | */ 186 | public function customer() 187 | { 188 | if ($this->customer) { 189 | return $this->customer; 190 | } 191 | 192 | return $this->customer = Cashier::findBillable($this->paymentIntent->customer); 193 | } 194 | 195 | /** 196 | * The Stripe PaymentIntent instance. 197 | * 198 | * @param array $expand 199 | * @return \Stripe\PaymentIntent 200 | */ 201 | public function asStripePaymentIntent(array $expand = []) 202 | { 203 | if ($expand) { 204 | return $this->customer()->stripe()->paymentIntents->retrieve( 205 | $this->paymentIntent->id, ['expand' => $expand] 206 | ); 207 | } 208 | 209 | return $this->paymentIntent; 210 | } 211 | 212 | /** 213 | * Refresh the PaymentIntent instance from the Stripe API. 214 | * 215 | * @param array $expand 216 | * @return $this 217 | */ 218 | public function refresh(array $expand = []) 219 | { 220 | $this->paymentIntent = $this->asStripePaymentIntent($expand); 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * Get the instance as an array. 227 | * 228 | * @return array 229 | */ 230 | public function toArray() 231 | { 232 | return $this->asStripePaymentIntent()->toArray(); 233 | } 234 | 235 | /** 236 | * Convert the object to its JSON representation. 237 | * 238 | * @param int $options 239 | * @return string 240 | */ 241 | public function toJson($options = 0) 242 | { 243 | return json_encode($this->jsonSerialize(), $options); 244 | } 245 | 246 | /** 247 | * Convert the object into something JSON serializable. 248 | * 249 | * @return array 250 | */ 251 | #[\ReturnTypeWillChange] 252 | public function jsonSerialize() 253 | { 254 | return $this->toArray(); 255 | } 256 | 257 | /** 258 | * Dynamically get values from the Stripe object. 259 | * 260 | * @param string $key 261 | * @return mixed 262 | */ 263 | public function __get($key) 264 | { 265 | return $this->paymentIntent->{$key}; 266 | } 267 | 268 | /** 269 | * Dynamically pass missing methods to the PaymentIntent instance. 270 | * 271 | * @param string $method 272 | * @param array $parameters 273 | * @return mixed 274 | */ 275 | public function __call($method, $parameters) 276 | { 277 | return $this->forwardCallTo($this->paymentIntent, $method, $parameters); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/PaymentMethod.php: -------------------------------------------------------------------------------- 1 | customer)) { 40 | throw new LogicException('The payment method is not attached to a customer.'); 41 | } 42 | 43 | if ($owner->stripe_id !== $paymentMethod->customer) { 44 | throw InvalidPaymentMethod::invalidOwner($paymentMethod, $owner); 45 | } 46 | 47 | $this->owner = $owner; 48 | $this->paymentMethod = $paymentMethod; 49 | } 50 | 51 | /** 52 | * Delete the payment method. 53 | * 54 | * @return void 55 | */ 56 | public function delete() 57 | { 58 | $this->owner->deletePaymentMethod($this->paymentMethod); 59 | } 60 | 61 | /** 62 | * Get the Stripe model instance. 63 | * 64 | * @return \Illuminate\Database\Eloquent\Model 65 | */ 66 | public function owner() 67 | { 68 | return $this->owner; 69 | } 70 | 71 | /** 72 | * Get the Stripe PaymentMethod instance. 73 | * 74 | * @return \Stripe\PaymentMethod 75 | */ 76 | public function asStripePaymentMethod() 77 | { 78 | return $this->paymentMethod; 79 | } 80 | 81 | /** 82 | * Get the instance as an array. 83 | * 84 | * @return array 85 | */ 86 | public function toArray() 87 | { 88 | return $this->asStripePaymentMethod()->toArray(); 89 | } 90 | 91 | /** 92 | * Convert the object to its JSON representation. 93 | * 94 | * @param int $options 95 | * @return string 96 | */ 97 | public function toJson($options = 0) 98 | { 99 | return json_encode($this->jsonSerialize(), $options); 100 | } 101 | 102 | /** 103 | * Convert the object into something JSON serializable. 104 | * 105 | * @return array 106 | */ 107 | #[\ReturnTypeWillChange] 108 | public function jsonSerialize() 109 | { 110 | return $this->toArray(); 111 | } 112 | 113 | /** 114 | * Dynamically get values from the Stripe object. 115 | * 116 | * @param string $key 117 | * @return mixed 118 | */ 119 | public function __get($key) 120 | { 121 | return $this->paymentMethod->{$key}; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/PromotionCode.php: -------------------------------------------------------------------------------- 1 | promotionCode = $promotionCode; 28 | } 29 | 30 | /** 31 | * Get the coupon that belongs to the promotion code. 32 | * 33 | * @return \Laravel\Cashier\Coupon 34 | */ 35 | public function coupon() 36 | { 37 | return new Coupon($this->promotionCode->coupon); 38 | } 39 | 40 | /** 41 | * Get the Stripe PromotionCode instance. 42 | * 43 | * @return \Stripe\PromotionCode 44 | */ 45 | public function asStripePromotionCode() 46 | { 47 | return $this->promotionCode; 48 | } 49 | 50 | /** 51 | * Get the instance as an array. 52 | * 53 | * @return array 54 | */ 55 | public function toArray() 56 | { 57 | return $this->asStripePromotionCode()->toArray(); 58 | } 59 | 60 | /** 61 | * Convert the object to its JSON representation. 62 | * 63 | * @param int $options 64 | * @return string 65 | */ 66 | public function toJson($options = 0) 67 | { 68 | return json_encode($this->jsonSerialize(), $options); 69 | } 70 | 71 | /** 72 | * Convert the object into something JSON serializable. 73 | * 74 | * @return array 75 | */ 76 | #[\ReturnTypeWillChange] 77 | public function jsonSerialize() 78 | { 79 | return $this->toArray(); 80 | } 81 | 82 | /** 83 | * Dynamically get values from the Stripe object. 84 | * 85 | * @param string $key 86 | * @return mixed 87 | */ 88 | public function __get($key) 89 | { 90 | return $this->promotionCode->{$key}; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SubscriptionBuilder.php: -------------------------------------------------------------------------------- 1 | type = $type; 88 | $this->owner = $owner; 89 | 90 | foreach ((array) $prices as $price) { 91 | $this->price($price); 92 | } 93 | } 94 | 95 | /** 96 | * Set a price on the subscription builder. 97 | * 98 | * @param string|array $price 99 | * @param int|null $quantity 100 | * @return $this 101 | */ 102 | public function price($price, $quantity = 1) 103 | { 104 | $options = is_array($price) ? $price : ['price' => $price]; 105 | 106 | $quantity = $price['quantity'] ?? $quantity; 107 | 108 | if (! is_null($quantity)) { 109 | $options['quantity'] = $quantity; 110 | } 111 | 112 | if ($taxRates = $this->getPriceTaxRatesForPayload($price)) { 113 | $options['tax_rates'] = $taxRates; 114 | } 115 | 116 | if (isset($options['price'])) { 117 | $this->items[$options['price']] = $options; 118 | } else { 119 | $this->items[] = $options; 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Set a metered price on the subscription builder. 127 | * 128 | * @param string $price 129 | * @return $this 130 | */ 131 | public function meteredPrice($price) 132 | { 133 | return $this->price($price, null); 134 | } 135 | 136 | /** 137 | * Specify the quantity of a subscription item. 138 | * 139 | * @param int|null $quantity 140 | * @param string|null $price 141 | * @return $this 142 | */ 143 | public function quantity($quantity, $price = null) 144 | { 145 | if (is_null($price)) { 146 | if (count($this->items) > 1) { 147 | throw new InvalidArgumentException('Price is required when creating subscriptions with multiple prices.'); 148 | } 149 | 150 | $price = Arr::first($this->items)['price']; 151 | } 152 | 153 | return $this->price($price, $quantity); 154 | } 155 | 156 | /** 157 | * Specify the number of days of the trial. 158 | * 159 | * @param int $trialDays 160 | * @return $this 161 | */ 162 | public function trialDays($trialDays) 163 | { 164 | $this->trialExpires = Carbon::now()->addDays($trialDays); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Specify the ending date of the trial. 171 | * 172 | * @param \Carbon\Carbon|\Carbon\CarbonInterface $trialUntil 173 | * @return $this 174 | */ 175 | public function trialUntil($trialUntil) 176 | { 177 | $this->trialExpires = $trialUntil; 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * Force the trial to end immediately. 184 | * 185 | * @return $this 186 | */ 187 | public function skipTrial() 188 | { 189 | $this->skipTrial = true; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Change the billing cycle anchor on a subscription creation. 196 | * 197 | * @param \DateTimeInterface|int $date 198 | * @return $this 199 | */ 200 | public function anchorBillingCycleOn($date) 201 | { 202 | if ($date instanceof DateTimeInterface) { 203 | $date = $date->getTimestamp(); 204 | } 205 | 206 | $this->billingCycleAnchor = $date; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * The metadata to apply to a new subscription. 213 | * 214 | * @param array $metadata 215 | * @return $this 216 | */ 217 | public function withMetadata($metadata) 218 | { 219 | $this->metadata = (array) $metadata; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Add a new Stripe subscription to the Stripe model. 226 | * 227 | * @param array $customerOptions 228 | * @param array $subscriptionOptions 229 | * @return \Laravel\Cashier\Subscription 230 | * 231 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 232 | */ 233 | public function add(array $customerOptions = [], array $subscriptionOptions = []) 234 | { 235 | return $this->create(null, $customerOptions, $subscriptionOptions); 236 | } 237 | 238 | /** 239 | * Create a new Stripe subscription. 240 | * 241 | * @param \Stripe\PaymentMethod|string|null $paymentMethod 242 | * @param array $customerOptions 243 | * @param array $subscriptionOptions 244 | * @return \Laravel\Cashier\Subscription 245 | * 246 | * @throws \Exception 247 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 248 | */ 249 | public function create($paymentMethod = null, array $customerOptions = [], array $subscriptionOptions = []) 250 | { 251 | if (empty($this->items)) { 252 | throw new Exception('At least one price is required when starting subscriptions.'); 253 | } 254 | 255 | $stripeCustomer = $this->getStripeCustomer($paymentMethod, $customerOptions); 256 | 257 | $stripeSubscription = $this->owner->stripe()->subscriptions->create(array_merge( 258 | ['customer' => $stripeCustomer->id], 259 | $this->buildPayload(), 260 | $subscriptionOptions 261 | )); 262 | 263 | $subscription = $this->createSubscription($stripeSubscription); 264 | 265 | $this->handlePaymentFailure($subscription, $paymentMethod); 266 | 267 | return $subscription; 268 | } 269 | 270 | /** 271 | * Create a new Stripe subscription and send an invoice to the customer. 272 | * 273 | * @param array $customerOptions 274 | * @param array $subscriptionOptions 275 | * @return \Laravel\Cashier\Subscription 276 | * 277 | * @throws \Exception 278 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 279 | */ 280 | public function createAndSendInvoice(array $customerOptions = [], array $subscriptionOptions = []) 281 | { 282 | return $this->create(null, $customerOptions, array_merge([ 283 | 'days_until_due' => 30, 284 | ], $subscriptionOptions, [ 285 | 'collection_method' => 'send_invoice', 286 | ])); 287 | } 288 | 289 | /** 290 | * Create the Eloquent Subscription. 291 | * 292 | * @param \Stripe\Subscription $stripeSubscription 293 | * @return \Laravel\Cashier\Subscription 294 | */ 295 | protected function createSubscription(StripeSubscription $stripeSubscription) 296 | { 297 | if ($subscription = $this->owner->subscriptions()->where('stripe_id', $stripeSubscription->id)->first()) { 298 | return $subscription; 299 | } 300 | 301 | /** @var \Stripe\SubscriptionItem $firstItem */ 302 | $firstItem = $stripeSubscription->items->first(); 303 | $isSinglePrice = $stripeSubscription->items->count() === 1; 304 | 305 | /** @var \Laravel\Cashier\Subscription $subscription */ 306 | $subscription = $this->owner->subscriptions()->create([ 307 | 'type' => $this->type, 308 | 'stripe_id' => $stripeSubscription->id, 309 | 'stripe_status' => $stripeSubscription->status, 310 | 'stripe_price' => $isSinglePrice ? $firstItem->price->id : null, 311 | 'quantity' => $isSinglePrice ? ($firstItem->quantity ?? null) : null, 312 | 'trial_ends_at' => ! $this->skipTrial ? $this->trialExpires : null, 313 | 'ends_at' => null, 314 | ]); 315 | 316 | /** @var \Stripe\SubscriptionItem $item */ 317 | foreach ($stripeSubscription->items as $item) { 318 | $subscription->items()->create([ 319 | 'stripe_id' => $item->id, 320 | 'stripe_product' => $item->price->product, 321 | 'stripe_price' => $item->price->id, 322 | 'quantity' => $item->quantity ?? null, 323 | ]); 324 | } 325 | 326 | return $subscription; 327 | } 328 | 329 | /** 330 | * Begin a new Checkout Session. 331 | * 332 | * @param array $sessionOptions 333 | * @param array $customerOptions 334 | * @return \Laravel\Cashier\Checkout 335 | */ 336 | public function checkout(array $sessionOptions = [], array $customerOptions = []) 337 | { 338 | if (empty($this->items)) { 339 | throw new Exception('At least one price is required when starting subscriptions.'); 340 | } 341 | 342 | if (! $this->skipTrial && $this->trialExpires) { 343 | // Checkout Sessions are active for 24 hours after their creation and within that time frame the customer 344 | // can complete the payment at any time. Stripe requires the trial end at least 48 hours in the future 345 | // so that there is still at least a one day trial if your customer pays at the end of the 24 hours. 346 | // We also add 10 seconds of extra time to account for any delay with an API request onto Stripe. 347 | $minimumTrialPeriod = Carbon::now()->addHours(48)->addSeconds(10); 348 | 349 | $trialEnd = $this->trialExpires->gt($minimumTrialPeriod) ? $this->trialExpires : $minimumTrialPeriod; 350 | } else { 351 | $trialEnd = null; 352 | } 353 | 354 | $billingCycleAnchor = $trialEnd === null ? $this->billingCycleAnchor : null; 355 | 356 | $payload = array_filter([ 357 | 'line_items' => Collection::make($this->items)->values()->all(), 358 | 'mode' => 'subscription', 359 | 'subscription_data' => array_filter([ 360 | 'default_tax_rates' => $this->getTaxRatesForPayload(), 361 | 'trial_end' => $trialEnd ? $trialEnd->getTimestamp() : null, 362 | 'billing_cycle_anchor' => $billingCycleAnchor, 363 | 'proration_behavior' => $billingCycleAnchor ? $this->prorateBehavior() : null, 364 | 'metadata' => array_merge($this->metadata, [ 365 | 'name' => $this->type, 366 | 'type' => $this->type, 367 | ]), 368 | ]), 369 | ]); 370 | 371 | return Checkout::customer($this->owner, $this) 372 | ->create([], array_merge_recursive($payload, $sessionOptions), $customerOptions); 373 | } 374 | 375 | /** 376 | * Get the Stripe customer instance for the current user and payment method. 377 | * 378 | * @param \Stripe\PaymentMethod|string|null $paymentMethod 379 | * @param array $options 380 | * @return \Stripe\Customer 381 | */ 382 | protected function getStripeCustomer($paymentMethod = null, array $options = []) 383 | { 384 | $customer = $this->owner->createOrGetStripeCustomer($options); 385 | 386 | if ($paymentMethod) { 387 | $this->owner->updateDefaultPaymentMethod($paymentMethod); 388 | } 389 | 390 | return $customer; 391 | } 392 | 393 | /** 394 | * Build the payload for subscription creation. 395 | * 396 | * @return array 397 | */ 398 | protected function buildPayload() 399 | { 400 | $payload = array_filter([ 401 | 'automatic_tax' => $this->automaticTaxPayload(), 402 | 'billing_cycle_anchor' => $this->billingCycleAnchor, 403 | 'coupon' => $this->couponId, 404 | 'expand' => ['latest_invoice.payment_intent'], 405 | 'metadata' => $this->metadata, 406 | 'items' => Collection::make($this->items)->values()->all(), 407 | 'payment_behavior' => $this->paymentBehavior(), 408 | 'promotion_code' => $this->promotionCodeId, 409 | 'proration_behavior' => $this->prorateBehavior(), 410 | 'trial_end' => $this->getTrialEndForPayload(), 411 | 'off_session' => true, 412 | ]); 413 | 414 | if ($taxRates = $this->getTaxRatesForPayload()) { 415 | $payload['default_tax_rates'] = $taxRates; 416 | } 417 | 418 | return $payload; 419 | } 420 | 421 | /** 422 | * Get the trial ending date for the Stripe payload. 423 | * 424 | * @return int|string|null 425 | */ 426 | protected function getTrialEndForPayload() 427 | { 428 | if ($this->skipTrial) { 429 | return 'now'; 430 | } 431 | 432 | if ($this->trialExpires) { 433 | return $this->trialExpires->getTimestamp(); 434 | } 435 | } 436 | 437 | /** 438 | * Get the tax rates for the Stripe payload. 439 | * 440 | * @return array|null 441 | */ 442 | protected function getTaxRatesForPayload() 443 | { 444 | if ($taxRates = $this->owner->taxRates()) { 445 | return $taxRates; 446 | } 447 | } 448 | 449 | /** 450 | * Get the price tax rates for the Stripe payload. 451 | * 452 | * @param string $price 453 | * @return array|null 454 | */ 455 | protected function getPriceTaxRatesForPayload($price) 456 | { 457 | if ($taxRates = $this->owner->priceTaxRates()) { 458 | return $taxRates[$price] ?? null; 459 | } 460 | } 461 | 462 | /** 463 | * Get the items set on the subscription builder. 464 | * 465 | * @return array 466 | */ 467 | public function getItems() 468 | { 469 | return $this->items; 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/SubscriptionItem.php: -------------------------------------------------------------------------------- 1 | 'integer', 38 | ]; 39 | 40 | /** 41 | * Get the subscription that the item belongs to. 42 | * 43 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 44 | */ 45 | public function subscription() 46 | { 47 | $model = Cashier::$subscriptionModel; 48 | 49 | return $this->belongsTo($model, (new $model)->getForeignKey()); 50 | } 51 | 52 | /** 53 | * Increment the quantity of the subscription item. 54 | * 55 | * @param int $count 56 | * @return $this 57 | * 58 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 59 | */ 60 | public function incrementQuantity($count = 1) 61 | { 62 | $this->updateQuantity($this->quantity + $count); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Increment the quantity of the subscription item, and invoice immediately. 69 | * 70 | * @param int $count 71 | * @return $this 72 | * 73 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 74 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 75 | */ 76 | public function incrementAndInvoice($count = 1) 77 | { 78 | $this->alwaysInvoice(); 79 | 80 | $this->incrementQuantity($count); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Decrement the quantity of the subscription item. 87 | * 88 | * @param int $count 89 | * @return $this 90 | * 91 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 92 | */ 93 | public function decrementQuantity($count = 1) 94 | { 95 | $this->updateQuantity(max(1, $this->quantity - $count)); 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Update the quantity of the subscription item. 102 | * 103 | * @param int $quantity 104 | * @return $this 105 | * 106 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 107 | */ 108 | public function updateQuantity($quantity) 109 | { 110 | $this->subscription->guardAgainstIncomplete(); 111 | 112 | $stripeSubscriptionItem = $this->updateStripeSubscriptionItem([ 113 | 'payment_behavior' => $this->paymentBehavior(), 114 | 'proration_behavior' => $this->prorateBehavior(), 115 | 'quantity' => $quantity, 116 | ]); 117 | 118 | $this->fill([ 119 | 'quantity' => $stripeSubscriptionItem->quantity, 120 | ])->save(); 121 | 122 | $stripeSubscription = $this->subscription->asStripeSubscription(); 123 | 124 | if ($this->subscription->hasSinglePrice()) { 125 | $this->subscription->fill([ 126 | 'quantity' => $stripeSubscriptionItem->quantity, 127 | ]); 128 | } 129 | 130 | $this->subscription->fill([ 131 | 'stripe_status' => $stripeSubscription->status, 132 | ])->save(); 133 | 134 | $this->handlePaymentFailure($this->subscription); 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Swap the subscription item to a new Stripe price. 141 | * 142 | * @param string $price 143 | * @param array $options 144 | * @return $this 145 | * 146 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 147 | */ 148 | public function swap($price, array $options = []) 149 | { 150 | $this->subscription->guardAgainstIncomplete(); 151 | 152 | $stripeSubscriptionItem = $this->updateStripeSubscriptionItem(array_merge( 153 | array_filter([ 154 | 'price' => $price, 155 | 'quantity' => $this->quantity, 156 | 'payment_behavior' => $this->paymentBehavior(), 157 | 'proration_behavior' => $this->prorateBehavior(), 158 | 'tax_rates' => $this->subscription->getPriceTaxRatesForPayload($price), 159 | ], function ($value) { 160 | return ! is_null($value); 161 | }), 162 | $options)); 163 | 164 | $this->fill([ 165 | 'stripe_product' => $stripeSubscriptionItem->price->product, 166 | 'stripe_price' => $stripeSubscriptionItem->price->id, 167 | 'quantity' => $stripeSubscriptionItem->quantity, 168 | ])->save(); 169 | 170 | $stripeSubscription = $this->subscription->asStripeSubscription(); 171 | 172 | if ($this->subscription->hasSinglePrice()) { 173 | $this->subscription->fill([ 174 | 'stripe_price' => $price, 175 | 'quantity' => $stripeSubscriptionItem->quantity, 176 | ]); 177 | } 178 | 179 | $this->subscription->fill([ 180 | 'stripe_status' => $stripeSubscription->status, 181 | ])->save(); 182 | 183 | $this->handlePaymentFailure($this->subscription); 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Swap the subscription item to a new Stripe price, and invoice immediately. 190 | * 191 | * @param string $price 192 | * @param array $options 193 | * @return $this 194 | * 195 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 196 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 197 | */ 198 | public function swapAndInvoice($price, array $options = []) 199 | { 200 | $this->alwaysInvoice(); 201 | 202 | return $this->swap($price, $options); 203 | } 204 | 205 | /** 206 | * Report usage for a metered product. 207 | * 208 | * @param int $quantity 209 | * @param \DateTimeInterface|int|null $timestamp 210 | * @return \Stripe\UsageRecord 211 | */ 212 | public function reportUsage($quantity = 1, $timestamp = null) 213 | { 214 | $timestamp = $timestamp instanceof DateTimeInterface ? $timestamp->getTimestamp() : $timestamp; 215 | 216 | return $this->subscription->owner->stripe()->subscriptionItems->createUsageRecord($this->stripe_id, [ 217 | 'quantity' => $quantity, 218 | 'action' => $timestamp ? 'set' : 'increment', 219 | 'timestamp' => $timestamp ?? time(), 220 | ]); 221 | } 222 | 223 | /** 224 | * Get the usage records for a metered product. 225 | * 226 | * @param array $options 227 | * @return \Illuminate\Support\Collection 228 | */ 229 | public function usageRecords($options = []) 230 | { 231 | return new Collection($this->subscription->owner->stripe()->subscriptionItems->allUsageRecordSummaries( 232 | $this->stripe_id, $options 233 | )->data); 234 | } 235 | 236 | /** 237 | * Update the underlying Stripe subscription item information for the model. 238 | * 239 | * @param array $options 240 | * @return \Stripe\SubscriptionItem 241 | */ 242 | public function updateStripeSubscriptionItem(array $options = []) 243 | { 244 | return $this->subscription->owner->stripe()->subscriptionItems->update( 245 | $this->stripe_id, $options 246 | ); 247 | } 248 | 249 | /** 250 | * Get the subscription as a Stripe subscription item object. 251 | * 252 | * @param array $expand 253 | * @return \Stripe\SubscriptionItem 254 | */ 255 | public function asStripeSubscriptionItem(array $expand = []) 256 | { 257 | return $this->subscription->owner->stripe()->subscriptionItems->retrieve( 258 | $this->stripe_id, ['expand' => $expand] 259 | ); 260 | } 261 | 262 | /** 263 | * Create a new factory instance for the model. 264 | * 265 | * @return \Illuminate\Database\Eloquent\Factories\Factory 266 | */ 267 | protected static function newFactory() 268 | { 269 | return SubscriptionItemFactory::new(); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Tax.php: -------------------------------------------------------------------------------- 1 | amount = $amount; 41 | $this->currency = $currency; 42 | $this->taxRate = $taxRate; 43 | } 44 | 45 | /** 46 | * Get the applied currency. 47 | * 48 | * @return string 49 | */ 50 | public function currency() 51 | { 52 | return $this->currency; 53 | } 54 | 55 | /** 56 | * Get the total tax that was paid (or will be paid). 57 | * 58 | * @return string 59 | */ 60 | public function amount() 61 | { 62 | return $this->formatAmount($this->amount); 63 | } 64 | 65 | /** 66 | * Get the raw total tax that was paid (or will be paid). 67 | * 68 | * @return int 69 | */ 70 | public function rawAmount() 71 | { 72 | return $this->amount; 73 | } 74 | 75 | /** 76 | * Format the given amount into a displayable currency. 77 | * 78 | * @param int $amount 79 | * @return string 80 | */ 81 | protected function formatAmount($amount) 82 | { 83 | return Cashier::formatAmount($amount, $this->currency); 84 | } 85 | 86 | /** 87 | * Determine if the tax is inclusive or not. 88 | * 89 | * @return bool 90 | */ 91 | public function isInclusive() 92 | { 93 | return $this->taxRate->inclusive; 94 | } 95 | 96 | /** 97 | * @return \Stripe\TaxRate 98 | */ 99 | public function taxRate() 100 | { 101 | return $this->taxRate; 102 | } 103 | 104 | /** 105 | * Dynamically get values from the Stripe object. 106 | * 107 | * @param string $key 108 | * @return mixed 109 | */ 110 | public function __get($key) 111 | { 112 | return $this->taxRate->{$key}; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | providers: 2 | - Laravel\Cashier\CashierServiceProvider 3 | 4 | migrations: 5 | - database/migrations 6 | 7 | workbench: 8 | build: 9 | - create-sqlite-db 10 | - db:wipe 11 | - migrate:fresh 12 | --------------------------------------------------------------------------------