├── routes └── web.php ├── src ├── Concerns │ ├── InteractsWithStripe.php │ ├── Prorates.php │ ├── InteractsWithPaymentBehavior.php │ ├── HandlesTaxes.php │ ├── ManagesUsageBilling.php │ ├── AllowsCoupons.php │ ├── HandlesPaymentFailures.php │ ├── PerformsCharges.php │ ├── ManagesSubscriptions.php │ ├── ManagesPaymentMethods.php │ └── ManagesInvoices.php ├── Contracts │ └── InvoiceRenderer.php ├── Exceptions │ ├── CustomerAlreadyCreated.php │ ├── InvalidCustomer.php │ ├── InvalidInvoice.php │ ├── InvalidCustomerBalanceTransaction.php │ ├── InvalidPaymentMethod.php │ ├── InvalidCoupon.php │ ├── SubscriptionUpdateFailure.php │ └── IncompletePayment.php ├── Events │ ├── WebhookHandled.php │ └── WebhookReceived.php ├── Logger.php ├── Billable.php ├── Http │ ├── Middleware │ │ ├── VerifyRedirectUrl.php │ │ └── VerifyWebhookSignature.php │ └── Controllers │ │ ├── PaymentController.php │ │ └── WebhookController.php ├── Invoices │ └── DompdfInvoiceRenderer.php ├── Jobs │ └── SyncCustomerDetails.php ├── Notifications │ └── ConfirmPayment.php ├── PromotionCode.php ├── Console │ └── WebhookCommand.php ├── Tax.php ├── PaymentMethod.php ├── CheckoutBuilder.php ├── InvoicePayment.php ├── Discount.php ├── Coupon.php ├── CashierServiceProvider.php ├── CustomerBalanceTransaction.php ├── Checkout.php ├── Cashier.php ├── Payment.php ├── SubscriptionItem.php └── InvoiceLineItem.php ├── testbench.yaml ├── database ├── migrations │ ├── 2025_06_06_000004_add_meter_id_to_subscription_items_table.php │ ├── 2025_06_06_000005_add_meter_event_name_to_subscription_items_table.php │ ├── 2019_05_03_000003_create_subscription_items_table.php │ ├── 2019_05_03_000001_create_customer_columns.php │ └── 2019_05_03_000002_create_subscriptions_table.php └── factories │ ├── SubscriptionItemFactory.php │ └── SubscriptionFactory.php ├── LICENSE.md ├── README.md ├── composer.json ├── config └── cashier.php └── resources └── views └── invoice.blade.php /routes/web.php: -------------------------------------------------------------------------------- 1 | name('payment'); 6 | Route::post('webhook', 'WebhookController@handleWebhook')->name('webhook'); 7 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithStripe.php: -------------------------------------------------------------------------------- 1 | stripe_id}."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidCustomer.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Events/WebhookReceived.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidInvoice.php: -------------------------------------------------------------------------------- 1 | id}` does not belong to this customer `$owner->stripe_id`."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | logger->error($message, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Billable.php: -------------------------------------------------------------------------------- 1 | id}` does not belong to 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 | -------------------------------------------------------------------------------- /database/migrations/2025_06_06_000004_add_meter_id_to_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | string('meter_id')->nullable()->after('stripe_price'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('subscription_items', function (Blueprint $table) { 25 | $table->dropColumn('meter_id'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_06_06_000005_add_meter_event_name_to_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | string('meter_event_name')->nullable()->after('quantity'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('subscription_items', function (Blueprint $table) { 25 | $table->dropColumn('meter_event_name'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 'meter_id' => null, 32 | 'quantity' => null, 33 | 'meter_event_name' => null, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Exceptions/InvalidCoupon.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, string $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/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(string $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(): string 69 | { 70 | return $this->prorationBehavior; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/PromotionCode.php: -------------------------------------------------------------------------------- 1 | promotionCode->coupon); 31 | } 32 | 33 | /** 34 | * Get the Stripe PromotionCode instance. 35 | * 36 | * @return \Stripe\PromotionCode 37 | */ 38 | public function asStripePromotionCode(): StripePromotionCode 39 | { 40 | return $this->promotionCode; 41 | } 42 | 43 | /** 44 | * Get the instance as an array. 45 | * 46 | * @return array 47 | */ 48 | public function toArray() 49 | { 50 | return $this->asStripePromotionCode()->toArray(); 51 | } 52 | 53 | /** 54 | * Convert the object to its JSON representation. 55 | * 56 | * @param int $options 57 | * @return string 58 | */ 59 | public function toJson($options = 0) 60 | { 61 | return json_encode($this->jsonSerialize(), $options); 62 | } 63 | 64 | /** 65 | * Convert the object into something JSON serializable. 66 | * 67 | * @return array 68 | */ 69 | #[\ReturnTypeWillChange] 70 | public function jsonSerialize() 71 | { 72 | return $this->toArray(); 73 | } 74 | 75 | /** 76 | * Dynamically get values from the Stripe object. 77 | * 78 | * @param string $key 79 | * @return mixed 80 | */ 81 | public function __get(string $key) 82 | { 83 | return $this->promotionCode->{$key}; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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/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/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/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(): string 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(string $paymentBehavior) 81 | { 82 | $this->paymentBehavior = $paymentBehavior; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Tax.php: -------------------------------------------------------------------------------- 1 | currency; 33 | } 34 | 35 | /** 36 | * Get the total tax that was paid (or will be paid). 37 | * 38 | * @return string 39 | */ 40 | public function amount(): string 41 | { 42 | return $this->formatAmount($this->amount); 43 | } 44 | 45 | /** 46 | * Get the raw total tax that was paid (or will be paid). 47 | * 48 | * @return int 49 | */ 50 | public function rawAmount(): int 51 | { 52 | return $this->amount; 53 | } 54 | 55 | /** 56 | * Format the given amount into a displayable currency. 57 | * 58 | * @param int $amount 59 | * @return string 60 | */ 61 | protected function formatAmount(int $amount): string 62 | { 63 | return Cashier::formatAmount($amount, $this->currency); 64 | } 65 | 66 | /** 67 | * Determine if the tax is inclusive or not. 68 | * 69 | * @return bool 70 | */ 71 | public function isInclusive(): bool 72 | { 73 | return $this->taxRate instanceof StripeTaxRate 74 | ? $this->taxRate->inclusive 75 | : false; 76 | } 77 | 78 | /** 79 | * Get the Stripe TaxRate object. 80 | * 81 | * @return \Stripe\TaxRate|null 82 | */ 83 | public function taxRate(): ?StripeTaxRate 84 | { 85 | return $this->taxRate; 86 | } 87 | 88 | /** 89 | * Dynamically get values from the Stripe object. 90 | * 91 | * @param string $key 92 | * @return mixed 93 | */ 94 | public function __get(string $key) 95 | { 96 | return $this->taxRate instanceof StripeTaxRate && in_array($key, $this->taxRate->keys()) 97 | ? $this->taxRate->{$key} 98 | : null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Concerns/HandlesTaxes.php: -------------------------------------------------------------------------------- 1 | customerIpAddress = $ipAddress; 39 | 40 | return $this; 41 | } 42 | 43 | /** 44 | * Set a pre-collected billing address used to estimate tax rates when performing "one-off" charges. 45 | * 46 | * @param string $country 47 | * @param string|null $postalCode 48 | * @param string|null $state 49 | * @return $this 50 | */ 51 | public function withTaxAddress(string $country, ?string $postalCode = null, ?string $state = null) 52 | { 53 | $this->estimationBillingAddress = array_filter([ 54 | 'country' => $country, 55 | 'postal_code' => $postalCode, 56 | 'state' => $state, 57 | ]); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Get the payload for Stripe automatic tax calculation. 64 | * 65 | * @return array|null 66 | */ 67 | protected function automaticTaxPayload(): ?array 68 | { 69 | return array_filter([ 70 | 'customer_ip_address' => $this->customerIpAddress, 71 | 'enabled' => $this->isAutomaticTaxEnabled(), 72 | 'estimation_billing_address' => $this->estimationBillingAddress, 73 | ]); 74 | } 75 | 76 | /** 77 | * Determine if automatic tax is enabled. 78 | * 79 | * @return bool 80 | */ 81 | protected function isAutomaticTaxEnabled(): bool 82 | { 83 | return Cashier::$calculatesTaxes; 84 | } 85 | 86 | /** 87 | * Indicate that Tax IDs should be collected during a Stripe Checkout session. 88 | * 89 | * @return $this 90 | */ 91 | public function collectTaxIds() 92 | { 93 | $this->collectTaxIds = true; 94 | 95 | return $this; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Concerns/ManagesUsageBilling.php: -------------------------------------------------------------------------------- 1 | stripe()->billing->meters->all($options, $requestOptions)->data); 22 | } 23 | 24 | /** 25 | * Report usage for a metered product. 26 | * 27 | * @param string $meter 28 | * @param int $quantity 29 | * @param string|null $price 30 | * @param array $options 31 | * @param array $requestOptions 32 | * @return \Stripe\V2\Billing\MeterEvent 33 | */ 34 | public function reportMeterEvent(string $meter, int $quantity = 1, array $options = [], array $requestOptions = []) 35 | { 36 | $this->assertCustomerExists(); 37 | 38 | /** @var \Stripe\Service\V2\Billing\MeterEventService $meterEventsService */ 39 | $meterEventsService = static::stripe()->v2->billing->meterEvents; 40 | 41 | return $meterEventsService->create([ 42 | 'event_name' => $meter, 43 | 'payload' => [ 44 | 'stripe_customer_id' => $this->stripeId(), 45 | 'value' => (string) $quantity, 46 | ], 47 | 'identifier' => Str::uuid()->toString(), 48 | ...$options, 49 | ], $requestOptions); 50 | } 51 | 52 | /** 53 | * Get the usage records for a meter using its ID. 54 | * 55 | * @param string $meterId 56 | * @param array $options 57 | * @param array $requestOptions 58 | * @return \Illuminate\Support\Collection 59 | */ 60 | public function meterEventSummaries(string $meterId, int $startTime = 1, ?int $endTime = null, array $options = [], array $requestOptions = []): Collection 61 | { 62 | $this->assertCustomerExists(); 63 | 64 | if (! isset($endTime)) { 65 | $endTime = time(); 66 | } 67 | 68 | /** @var \Stripe\Service\Billing\MeterService $metersService */ 69 | $metersService = static::stripe()->billing->meters; 70 | 71 | return new Collection($metersService->allEventSummaries( 72 | $meterId, 73 | [ 74 | 'customer' => $this->stripeId(), 75 | 'start_time' => $startTime, 76 | 'end_time' => $endTime, 77 | ...$options, 78 | ], 79 | $requestOptions 80 | )->data); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/PaymentMethod.php: -------------------------------------------------------------------------------- 1 | customer)) { 27 | throw new LogicException('The payment method is not attached to a customer.'); 28 | } 29 | 30 | if ($owner->stripe_id !== $paymentMethod->customer) { 31 | throw InvalidPaymentMethod::invalidOwner($paymentMethod, $owner); 32 | } 33 | } 34 | 35 | /** 36 | * Delete the payment method. 37 | * 38 | * @return void 39 | */ 40 | public function delete(): void 41 | { 42 | $this->owner->deletePaymentMethod($this->paymentMethod); 43 | } 44 | 45 | /** 46 | * Get the Stripe model instance. 47 | * 48 | * @return \Illuminate\Database\Eloquent\Model 49 | */ 50 | public function owner() 51 | { 52 | return $this->owner; 53 | } 54 | 55 | /** 56 | * Get the Stripe PaymentMethod instance. 57 | * 58 | * @return \Stripe\PaymentMethod 59 | */ 60 | public function asStripePaymentMethod() 61 | { 62 | return $this->paymentMethod; 63 | } 64 | 65 | /** 66 | * Get the instance as an array. 67 | * 68 | * @return array 69 | */ 70 | public function toArray() 71 | { 72 | return $this->asStripePaymentMethod()->toArray(); 73 | } 74 | 75 | /** 76 | * Convert the object to its JSON representation. 77 | * 78 | * @param int $options 79 | * @return string 80 | */ 81 | public function toJson($options = 0) 82 | { 83 | return json_encode($this->jsonSerialize(), $options); 84 | } 85 | 86 | /** 87 | * Convert the object into something JSON serializable. 88 | * 89 | * @return array 90 | */ 91 | #[\ReturnTypeWillChange] 92 | public function jsonSerialize() 93 | { 94 | return $this->toArray(); 95 | } 96 | 97 | /** 98 | * Dynamically get values from the Stripe object. 99 | * 100 | * @param string $key 101 | * @return mixed 102 | */ 103 | public function __get(string $key) 104 | { 105 | return $this->paymentMethod->{$key}; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/CheckoutBuilder.php: -------------------------------------------------------------------------------- 1 | couponId = $parentInstance->couponId; 25 | $this->promotionCodeId = $parentInstance->promotionCodeId; 26 | $this->allowPromotionCodes = $parentInstance->allowPromotionCodes; 27 | } 28 | 29 | if ($parentInstance && in_array(HandlesTaxes::class, class_uses_recursive($parentInstance))) { 30 | $this->customerIpAddress = $parentInstance->customerIpAddress; 31 | $this->estimationBillingAddress = $parentInstance->estimationBillingAddress; 32 | $this->collectTaxIds = $parentInstance->collectTaxIds; 33 | } 34 | } 35 | 36 | /** 37 | * Create a new checkout builder instance. 38 | * 39 | * @param \Illuminate\Database\Eloquent\Model|null $owner 40 | * @param object|null $instance 41 | * @return \Laravel\Cashier\CheckoutBuilder 42 | */ 43 | public static function make($owner = null, ?object $instance = null) 44 | { 45 | return new static($owner, $instance); 46 | } 47 | 48 | /** 49 | * Create a new checkout session. 50 | * 51 | * @param array|string $items 52 | * @param array $sessionOptions 53 | * @param array $customerOptions 54 | * @return \Laravel\Cashier\Checkout 55 | */ 56 | public function create(string|array $items, array $sessionOptions = [], array $customerOptions = []): Checkout 57 | { 58 | $payload = array_filter([ 59 | 'allow_promotion_codes' => $this->allowPromotionCodes, 60 | 'automatic_tax' => $this->automaticTaxPayload(), 61 | 'discounts' => $this->checkoutDiscounts(), 62 | 'line_items' => Collection::make((array) $items)->map(function ($item, $key) { 63 | if (is_string($key)) { 64 | return ['price' => $key, 'quantity' => $item]; 65 | } 66 | 67 | $item = is_string($item) ? ['price' => $item] : $item; 68 | 69 | $item['quantity'] = $item['quantity'] ?? 1; 70 | 71 | return $item; 72 | })->values()->all(), 73 | 'tax_id_collection' => (Cashier::$calculatesTaxes ?: $this->collectTaxIds) 74 | ? ['enabled' => true] 75 | : [], 76 | ]); 77 | 78 | return Checkout::create($this->owner, array_merge($payload, $sessionOptions), $customerOptions); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Concerns/AllowsCoupons.php: -------------------------------------------------------------------------------- 1 | couponId = $couponId; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * The promotion code ID to apply. 43 | * 44 | * @return $this 45 | */ 46 | public function withPromotionCode(?string $promotionCodeId) 47 | { 48 | $this->promotionCodeId = $promotionCodeId; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Enables user redeemable promotion codes for a Stripe Checkout session. 55 | * 56 | * @return $this 57 | */ 58 | public function allowPromotionCodes() 59 | { 60 | $this->allowPromotionCodes = true; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Return the discounts for a Stripe Checkout session. 67 | * 68 | * @return array[]|null 69 | * 70 | * @throws \Laravel\Cashier\Exceptions\InvalidCoupon 71 | */ 72 | protected function checkoutDiscounts(): ?array 73 | { 74 | $discounts = []; 75 | 76 | if ($this->couponId) { 77 | $this->validateCouponForCheckout($this->couponId); 78 | 79 | $discounts[] = ['coupon' => $this->couponId]; 80 | } 81 | 82 | if ($this->promotionCodeId) { 83 | $discounts[] = ['promotion_code' => $this->promotionCodeId]; 84 | } 85 | 86 | return ! empty($discounts) ? $discounts : null; 87 | } 88 | 89 | /** 90 | * Validate that a coupon can be used in checkout sessions. 91 | * 92 | * @param string $couponId 93 | * @return void 94 | * 95 | * @throws \Laravel\Cashier\Exceptions\InvalidCoupon 96 | * @throws \Stripe\Exception\ApiErrorException 97 | */ 98 | protected function validateCouponForCheckout(string $couponId): void 99 | { 100 | /** @var \Stripe\Service\CouponService $couponsService */ 101 | $couponsService = static::stripe()->coupons; 102 | 103 | $stripeCoupon = $couponsService->retrieve($couponId); 104 | 105 | $coupon = new Coupon($stripeCoupon); 106 | 107 | if ($coupon->isForeverAmountOff()) { 108 | throw InvalidCoupon::cannotUseForeverAmountOffInCheckout($couponId); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/InvoicePayment.php: -------------------------------------------------------------------------------- 1 | rawAmount(), $this->currency()); 31 | } 32 | 33 | /** 34 | * Get the raw allocated amount. 35 | * 36 | * @return int 37 | */ 38 | public function rawAmount(): int 39 | { 40 | return $this->invoicePayment->amount_paid ?? $this->invoicePayment->amount_requested; 41 | } 42 | 43 | /** 44 | * Get the currency of the payment. 45 | * 46 | * @return string 47 | */ 48 | public function currency(): string 49 | { 50 | return $this->invoicePayment->currency; 51 | } 52 | 53 | /** 54 | * Get the payment status. 55 | * 56 | * @return string 57 | */ 58 | public function status(): string 59 | { 60 | return $this->invoicePayment->status; 61 | } 62 | 63 | /** 64 | * Determine if the payment is completed. 65 | * 66 | * @return bool 67 | */ 68 | public function isCompleted(): bool 69 | { 70 | return $this->invoicePayment->status === 'paid'; 71 | } 72 | 73 | /** 74 | * Get the Stripe InvoicePayment instance. 75 | * 76 | * @return \Stripe\InvoicePayment 77 | */ 78 | public function asStripeInvoicePayment() 79 | { 80 | return $this->invoicePayment; 81 | } 82 | 83 | /** 84 | * Get the instance as an array. 85 | * 86 | * @return array 87 | */ 88 | public function toArray() 89 | { 90 | return $this->asStripeInvoicePayment()->toArray(); 91 | } 92 | 93 | /** 94 | * Convert the object to its JSON representation. 95 | * 96 | * @param int $options 97 | * @return string 98 | */ 99 | public function toJson($options = 0) 100 | { 101 | return json_encode($this->jsonSerialize(), $options); 102 | } 103 | 104 | /** 105 | * Convert the object into something JSON serializable. 106 | * 107 | * @return array 108 | */ 109 | #[\ReturnTypeWillChange] 110 | public function jsonSerialize() 111 | { 112 | return $this->toArray(); 113 | } 114 | 115 | /** 116 | * Dynamically get values from the Stripe object. 117 | * 118 | * @param string $key 119 | * @return mixed 120 | */ 121 | public function __get(string $key) 122 | { 123 | return $this->invoicePayment->{$key}; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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": "^17.3.0", 36 | "symfony/console": "^6.0|^7.0", 37 | "symfony/http-kernel": "^6.0|^7.0", 38 | "symfony/polyfill-intl-icu": "^1.22.1", 39 | "symfony/polyfill-php84": "^1.32" 40 | }, 41 | "require-dev": { 42 | "dompdf/dompdf": "^2.0|^3.0", 43 | "orchestra/testbench": "^8.36|^9.15|^10.8", 44 | "phpstan/phpstan": "^1.10", 45 | "spatie/laravel-ray": "^1.40" 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 (^2.0|^3.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 | "App\\": "workbench/app/", 61 | "Database\\Factories\\": "workbench/database/factories/", 62 | "Database\\Seeders\\": "workbench/database/seeders/" 63 | } 64 | }, 65 | "extra": { 66 | "branch-alias": { 67 | "dev-master": "16.x-dev" 68 | }, 69 | "laravel": { 70 | "providers": [ 71 | "Laravel\\Cashier\\CashierServiceProvider" 72 | ] 73 | } 74 | }, 75 | "config": { 76 | "audit": { 77 | "block-insecure": false 78 | }, 79 | "sort-packages": true 80 | }, 81 | "scripts": { 82 | "post-autoload-dump": [ 83 | "@clear", 84 | "@prepare" 85 | ], 86 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 87 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 88 | "build": "@php vendor/bin/testbench workbench:build --ansi", 89 | "serve": [ 90 | "Composer\\Config::disableProcessTimeout", 91 | "@build", 92 | "@php vendor/bin/testbench serve --ansi" 93 | ], 94 | "lint": [ 95 | "@php vendor/bin/phpstan analyse --verbose --ansi" 96 | ], 97 | "test": [ 98 | "@clear", 99 | "@php vendor/bin/phpunit" 100 | ] 101 | }, 102 | "minimum-stability": "dev", 103 | "prefer-stable": true 104 | } 105 | -------------------------------------------------------------------------------- /src/Discount.php: -------------------------------------------------------------------------------- 1 | discount->coupon); 34 | } 35 | 36 | /** 37 | * Get the promotion code applied to create this discount. 38 | * 39 | * @return \Laravel\Cashier\PromotionCode|null 40 | */ 41 | public function promotionCode(): ?PromotionCode 42 | { 43 | if (is_null($this->discount->promotion_code)) { 44 | return null; 45 | } 46 | 47 | // If promotion_code is already expanded as an object, use it... 48 | if (is_object($this->discount->promotion_code) && isset($this->discount->promotion_code->id)) { 49 | return new PromotionCode($this->discount->promotion_code); 50 | } 51 | 52 | // If promotion_code is just an ID string, fetch it from Stripe... 53 | if (is_string($this->discount->promotion_code)) { 54 | $promotionCode = StripePromotionCode::retrieve($this->discount->promotion_code); 55 | 56 | return new PromotionCode($promotionCode); 57 | } 58 | 59 | return null; 60 | } 61 | 62 | /** 63 | * Get the date that the coupon was applied. 64 | * 65 | * @return \Carbon\CarbonInterface 66 | */ 67 | public function start(): CarbonInterface 68 | { 69 | return Carbon::createFromTimestamp($this->discount->start); 70 | } 71 | 72 | /** 73 | * Get the date that this discount will end. 74 | * 75 | * @return \Carbon\CarbonInterface|null 76 | */ 77 | public function end(): ?CarbonInterface 78 | { 79 | if (! is_null($this->discount->end)) { 80 | return Carbon::createFromTimestamp($this->discount->end); 81 | } 82 | 83 | return null; 84 | } 85 | 86 | /** 87 | * Get the Stripe Discount instance. 88 | * 89 | * @return \Stripe\Discount 90 | */ 91 | public function asStripeDiscount() 92 | { 93 | return $this->discount; 94 | } 95 | 96 | /** 97 | * Get the instance as an array. 98 | * 99 | * @return array 100 | */ 101 | public function toArray() 102 | { 103 | return $this->asStripeDiscount()->toArray(); 104 | } 105 | 106 | /** 107 | * Convert the object to its JSON representation. 108 | * 109 | * @param int $options 110 | * @return string 111 | */ 112 | public function toJson($options = 0) 113 | { 114 | return json_encode($this->jsonSerialize(), $options); 115 | } 116 | 117 | /** 118 | * Convert the object into something JSON serializable. 119 | * 120 | * @return array 121 | */ 122 | #[\ReturnTypeWillChange] 123 | public function jsonSerialize() 124 | { 125 | return $this->toArray(); 126 | } 127 | 128 | /** 129 | * Dynamically get values from the Stripe object. 130 | * 131 | * @param string $key 132 | * @return mixed 133 | */ 134 | public function __get(string $key) 135 | { 136 | return $this->discount->{$key}; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /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 | 'payment_method' => $paymentMethod instanceof StripePaymentMethod 51 | ? $paymentMethod->id 52 | : $paymentMethod, 53 | ] 54 | )); 55 | } else { 56 | $paymentIntent = $e->payment->confirm($this->paymentConfirmationOptions); 57 | } 58 | } catch (StripeCardException) { 59 | $paymentIntent = $e->payment->asStripePaymentIntent(); 60 | } 61 | 62 | // Since the invoice field is no longer available on payment intent, we need to refresh the subscription directly... 63 | $stripeSubscription = $subscription->asStripeSubscription(); 64 | 65 | $subscription->fill([ 66 | 'stripe_status' => $stripeSubscription->status, 67 | ])->save(); 68 | 69 | if ($subscription->hasIncompletePayment()) { 70 | (new Payment($paymentIntent))->validate(); 71 | } 72 | } else { 73 | throw $e; 74 | } 75 | } 76 | } 77 | 78 | $this->confirmIncompletePayment = true; 79 | $this->paymentConfirmationOptions = []; 80 | } 81 | 82 | /** 83 | * Prevent automatic confirmation of incomplete payments. 84 | * 85 | * @return $this 86 | */ 87 | public function ignoreIncompletePayments() 88 | { 89 | $this->confirmIncompletePayment = false; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Specify the options to be used when confirming a payment intent. 96 | * 97 | * @param array $options 98 | * @return $this 99 | */ 100 | public function withPaymentConfirmationOptions(array $options) 101 | { 102 | $this->paymentConfirmationOptions = $options; 103 | 104 | return $this; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Coupon.php: -------------------------------------------------------------------------------- 1 | coupon->name ?: $this->coupon->id; 31 | } 32 | 33 | /** 34 | * Determine if the coupon is a percentage. 35 | * 36 | * @return bool 37 | */ 38 | public function isPercentage(): bool 39 | { 40 | return ! is_null($this->coupon->percent_off); 41 | } 42 | 43 | /** 44 | * Get the discount percentage for the invoice. 45 | * 46 | * @return float|null 47 | */ 48 | public function percentOff(): ?float 49 | { 50 | return $this->coupon->percent_off; 51 | } 52 | 53 | /** 54 | * Get the amount off for the coupon. 55 | * 56 | * @return string|null 57 | */ 58 | public function amountOff(): ?string 59 | { 60 | if (! is_null($this->coupon->amount_off)) { 61 | return $this->formatAmount($this->rawAmountOff()); 62 | } 63 | 64 | return null; 65 | } 66 | 67 | /** 68 | * Get the raw amount off for the coupon. 69 | * 70 | * @return int|null 71 | */ 72 | public function rawAmountOff(): ?int 73 | { 74 | return $this->coupon->amount_off; 75 | } 76 | 77 | /** 78 | * Determine if this is an amount_off coupon with forever duration. 79 | * 80 | * @return bool 81 | */ 82 | public function isForeverAmountOff(): bool 83 | { 84 | return ! is_null($this->coupon->amount_off) && $this->coupon->duration === 'forever'; 85 | } 86 | 87 | /** 88 | * Get the duration of the coupon. 89 | * 90 | * @return string 91 | */ 92 | public function duration(): string 93 | { 94 | return $this->coupon->duration; 95 | } 96 | 97 | /** 98 | * Format the given amount into a displayable currency. 99 | * 100 | * @param int $amount 101 | * @return string 102 | */ 103 | protected function formatAmount(int $amount): string 104 | { 105 | return Cashier::formatAmount($amount, $this->coupon->currency); 106 | } 107 | 108 | /** 109 | * Get the Stripe Coupon instance. 110 | * 111 | * @return \Stripe\Coupon 112 | */ 113 | public function asStripeCoupon() 114 | { 115 | return $this->coupon; 116 | } 117 | 118 | /** 119 | * Get the instance as an array. 120 | * 121 | * @return array 122 | */ 123 | public function toArray() 124 | { 125 | return $this->asStripeCoupon()->toArray(); 126 | } 127 | 128 | /** 129 | * Convert the object to its JSON representation. 130 | * 131 | * @param int $options 132 | * @return string 133 | */ 134 | public function toJson($options = 0) 135 | { 136 | return json_encode($this->jsonSerialize(), $options); 137 | } 138 | 139 | /** 140 | * Convert the object into something JSON serializable. 141 | * 142 | * @return array 143 | */ 144 | #[\ReturnTypeWillChange] 145 | public function jsonSerialize() 146 | { 147 | return $this->toArray(); 148 | } 149 | 150 | /** 151 | * Dynamically get values from the Stripe object. 152 | * 153 | * @param string $key 154 | * @return mixed 155 | */ 156 | public function __get(string $key) 157 | { 158 | return $this->coupon->{$key}; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /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(): void 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(): void 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(): void 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(): void 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(): void 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(): void 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(): void 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(): void 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(): void 157 | { 158 | if ($this->app->runningInConsole()) { 159 | $this->commands([ 160 | WebhookCommand::class, 161 | ]); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/CustomerBalanceTransaction.php: -------------------------------------------------------------------------------- 1 | stripe_id !== $transaction->customer) { 22 | throw InvalidCustomerBalanceTransaction::invalidOwner($transaction, $owner); 23 | } 24 | } 25 | 26 | /** 27 | * Get the total transaction amount. 28 | * 29 | * @return string 30 | */ 31 | public function amount(): string 32 | { 33 | return $this->formatAmount($this->rawAmount()); 34 | } 35 | 36 | /** 37 | * Get the raw total transaction amount. 38 | * 39 | * @return int 40 | */ 41 | public function rawAmount(): int 42 | { 43 | return $this->transaction->amount; 44 | } 45 | 46 | /** 47 | * Get the ending balance. 48 | * 49 | * @return string 50 | */ 51 | public function endingBalance(): string 52 | { 53 | return $this->formatAmount($this->rawEndingBalance()); 54 | } 55 | 56 | /** 57 | * Get the raw ending balance. 58 | * 59 | * @return int 60 | */ 61 | public function rawEndingBalance(): int 62 | { 63 | return $this->transaction->ending_balance; 64 | } 65 | 66 | /** 67 | * Get the balance type of the transaction. 68 | * 69 | * @return string|null 70 | */ 71 | public function balanceType(): ?string 72 | { 73 | return $this->transaction->balance_type; 74 | } 75 | 76 | /** 77 | * Get the checkout session ID for this transaction. 78 | * 79 | * @return string|null 80 | */ 81 | public function checkoutSession(): ?string 82 | { 83 | return $this->transaction->checkout_session; 84 | } 85 | 86 | /** 87 | * Determine if this transaction is a checkout session subscription payment. 88 | * 89 | * @return bool 90 | */ 91 | public function isCheckoutSessionSubscriptionPayment(): bool 92 | { 93 | return $this->transaction->balance_type === 'checkout_session_subscription_payment'; 94 | } 95 | 96 | /** 97 | * Determine if this transaction is a canceled checkout session subscription payment. 98 | * 99 | * @return bool 100 | */ 101 | public function isCheckoutSessionSubscriptionPaymentCanceled(): bool 102 | { 103 | return $this->transaction->balance_type === 'checkout_session_subscription_payment_canceled'; 104 | } 105 | 106 | /** 107 | * Format the given amount into a displayable currency. 108 | * 109 | * @param int $amount 110 | * @return string 111 | */ 112 | protected function formatAmount(int $amount): string 113 | { 114 | return Cashier::formatAmount($amount, $this->transaction->currency); 115 | } 116 | 117 | /** 118 | * Return the related invoice for this transaction. 119 | * 120 | * @return \Laravel\Cashier\Invoice 121 | */ 122 | public function invoice(): Invoice 123 | { 124 | return $this->transaction->invoice 125 | ? $this->owner->findInvoice($this->transaction->invoice) 126 | : null; 127 | } 128 | 129 | /** 130 | * Get the Stripe CustomerBalanceTransaction instance. 131 | * 132 | * @return \Stripe\CustomerBalanceTransaction 133 | */ 134 | public function asStripeCustomerBalanceTransaction() 135 | { 136 | return $this->transaction; 137 | } 138 | 139 | /** 140 | * Get the instance as an array. 141 | * 142 | * @return array 143 | */ 144 | public function toArray() 145 | { 146 | return $this->asStripeCustomerBalanceTransaction()->toArray(); 147 | } 148 | 149 | /** 150 | * Convert the object to its JSON representation. 151 | * 152 | * @param int $options 153 | * @return string 154 | */ 155 | public function toJson($options = 0) 156 | { 157 | return json_encode($this->jsonSerialize(), $options); 158 | } 159 | 160 | /** 161 | * Convert the object into something JSON serializable. 162 | * 163 | * @return array 164 | */ 165 | #[\ReturnTypeWillChange] 166 | public function jsonSerialize() 167 | { 168 | return $this->toArray(); 169 | } 170 | 171 | /** 172 | * Dynamically get values from the Stripe object. 173 | * 174 | * @param string $key 175 | * @return mixed 176 | */ 177 | public function __get(string $key) 178 | { 179 | return $this->transaction->{$key}; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Checkout.php: -------------------------------------------------------------------------------- 1 | Session::MODE_PAYMENT, 63 | ], $sessionOptions); 64 | 65 | if ($owner) { 66 | $data['customer'] = $owner->createOrGetStripeCustomer($customerOptions)->id; 67 | 68 | $stripe = $owner->stripe(); 69 | } else { 70 | $stripe = Cashier::stripe(); 71 | } 72 | 73 | // Make sure to collect address and name when Tax ID collection is enabled... 74 | if (isset($data['customer']) && ($data['tax_id_collection']['enabled'] ?? false)) { 75 | if (! isset($data['billing_address_collection'])) { 76 | $data['billing_address_collection'] = 'required'; 77 | } 78 | 79 | $data['customer_update']['name'] = 'auto'; 80 | } 81 | 82 | if ($data['mode'] === Session::MODE_PAYMENT && ($data['invoice_creation']['enabled'] ?? false)) { 83 | $data['invoice_creation']['invoice_data']['metadata']['is_on_session_checkout'] = true; 84 | } elseif ($data['mode'] === Session::MODE_SUBSCRIPTION) { 85 | $data['subscription_data']['metadata']['is_on_session_checkout'] = true; 86 | } 87 | 88 | // Remove success and cancel URLs if "ui_mode" is "embedded"... 89 | if (isset($data['ui_mode']) && $data['ui_mode'] === 'embedded') { 90 | $data['return_url'] = $sessionOptions['return_url'] ?? route('home'); 91 | 92 | // Remove return URL for embedded UI mode when no redirection is desired on completion... 93 | if (isset($data['redirect_on_completion']) && $data['redirect_on_completion'] === 'never') { 94 | unset($data['return_url']); 95 | } 96 | } else { 97 | $data['success_url'] = $sessionOptions['success_url'] ?? route('home').'?checkout=success'; 98 | $data['cancel_url'] = $sessionOptions['cancel_url'] ?? route('home').'?checkout=cancelled'; 99 | } 100 | 101 | $session = $stripe->checkout->sessions->create($data); 102 | 103 | return new static($owner, $session); 104 | } 105 | 106 | /** 107 | * Redirect to the checkout session. 108 | * 109 | * @return \Illuminate\Http\RedirectResponse 110 | */ 111 | public function redirect(): RedirectResponse 112 | { 113 | return Redirect::to($this->session->url, 303); 114 | } 115 | 116 | /** 117 | * Create an HTTP response that represents the object. 118 | * 119 | * @param \Illuminate\Http\Request $request 120 | * @return \Symfony\Component\HttpFoundation\Response 121 | */ 122 | public function toResponse($request) 123 | { 124 | return $this->redirect(); 125 | } 126 | 127 | /** 128 | * Get the Checkout Session as a Stripe Checkout Session object. 129 | * 130 | * @return \Stripe\Checkout\Session 131 | */ 132 | public function asStripeCheckoutSession() 133 | { 134 | return $this->session; 135 | } 136 | 137 | /** 138 | * Get the instance as an array. 139 | * 140 | * @return array 141 | */ 142 | public function toArray() 143 | { 144 | return $this->asStripeCheckoutSession()->toArray(); 145 | } 146 | 147 | /** 148 | * Convert the object to its JSON representation. 149 | * 150 | * @param int $options 151 | * @return string 152 | */ 153 | public function toJson($options = 0) 154 | { 155 | return json_encode($this->jsonSerialize(), $options); 156 | } 157 | 158 | /** 159 | * Convert the object into something JSON serializable. 160 | * 161 | * @return array 162 | */ 163 | #[\ReturnTypeWillChange] 164 | public function jsonSerialize() 165 | { 166 | return $this->toArray(); 167 | } 168 | 169 | /** 170 | * Dynamically get values from the Stripe object. 171 | * 172 | * @param string $key 173 | * @return mixed 174 | */ 175 | public function __get(string $key) 176 | { 177 | return $this->session->{$key}; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Concerns/PerformsCharges.php: -------------------------------------------------------------------------------- 1 | 'automatic', 28 | 'confirm' => true, 29 | ], $options); 30 | 31 | $options['payment_method'] = $paymentMethod; 32 | 33 | $payment = $this->createPayment($amount, $options); 34 | 35 | $payment->validate(); 36 | 37 | return $payment; 38 | } 39 | 40 | /** 41 | * Create a new PaymentIntent instance. 42 | * 43 | * @param int $amount 44 | * @param array $options 45 | * @return \Laravel\Cashier\Payment 46 | */ 47 | public function pay(int $amount, array $options = []): Payment 48 | { 49 | $options['automatic_payment_methods'] = ['enabled' => true]; 50 | 51 | unset($options['payment_method_types']); 52 | 53 | return $this->createPayment($amount, $options); 54 | } 55 | 56 | /** 57 | * Create a new PaymentIntent instance for the given payment method types. 58 | * 59 | * @param int $amount 60 | * @param array $paymentMethods 61 | * @param array $options 62 | * @return \Laravel\Cashier\Payment 63 | */ 64 | public function payWith(int $amount, array $paymentMethods, array $options = []): Payment 65 | { 66 | $options['payment_method_types'] = $paymentMethods; 67 | 68 | unset($options['automatic_payment_methods']); 69 | 70 | return $this->createPayment($amount, $options); 71 | } 72 | 73 | /** 74 | * Create a new Payment instance with a Stripe PaymentIntent. 75 | * 76 | * @param int $amount 77 | * @param array $options 78 | * @return \Laravel\Cashier\Payment 79 | */ 80 | public function createPayment(int $amount, array $options = []): Payment 81 | { 82 | $options = array_merge([ 83 | 'currency' => $this->preferredCurrency(), 84 | ], $options); 85 | 86 | $options['amount'] = $amount; 87 | 88 | if ($this->hasStripeId()) { 89 | $options['customer'] = $this->stripe_id; 90 | } 91 | 92 | /** @var \Stripe\Service\PaymentIntentService $paymentIntentsService */ 93 | $paymentIntentsService = static::stripe()->paymentIntents; 94 | 95 | return new Payment( 96 | $paymentIntentsService->create($options) 97 | ); 98 | } 99 | 100 | /** 101 | * Find a payment intent by ID. 102 | * 103 | * @param string $id 104 | * @return \Laravel\Cashier\Payment|null 105 | */ 106 | public function findPayment(string $id): ?Payment 107 | { 108 | $stripePaymentIntent = null; 109 | 110 | /** @var \Stripe\Service\PaymentIntentService $paymentIntentsService */ 111 | $paymentIntentsService = static::stripe()->paymentIntents; 112 | 113 | try { 114 | $stripePaymentIntent = $paymentIntentsService->retrieve($id); 115 | } catch (StripeInvalidRequestException $exception) { 116 | // 117 | } 118 | 119 | return $stripePaymentIntent ? new Payment($stripePaymentIntent) : null; 120 | } 121 | 122 | /** 123 | * Refund a customer for a charge. 124 | * 125 | * @param string $paymentIntent 126 | * @param array $options 127 | * @return \Stripe\Refund 128 | */ 129 | public function refund(string $paymentIntent, array $options = []) 130 | { 131 | /** @var \Stripe\Service\RefundService $refundsService */ 132 | $refundsService = static::stripe()->refunds; 133 | 134 | return $refundsService->create( 135 | ['payment_intent' => $paymentIntent] + $options 136 | ); 137 | } 138 | 139 | /** 140 | * Begin a new checkout session for existing prices. 141 | * 142 | * @param array|string $items 143 | * @param array $sessionOptions 144 | * @param array $customerOptions 145 | * @return \Laravel\Cashier\Checkout 146 | */ 147 | public function checkout(string|array $items, array $sessionOptions = [], array $customerOptions = []): Checkout 148 | { 149 | return Checkout::customer($this, $this)->create($items, $sessionOptions, $customerOptions); 150 | } 151 | 152 | /** 153 | * Begin a new checkout session for a "one-off" charge. 154 | * 155 | * @param int $amount 156 | * @param string $name 157 | * @param int $quantity 158 | * @param array $sessionOptions 159 | * @param array $customerOptions 160 | * @param array $productData 161 | * @return \Laravel\Cashier\Checkout 162 | */ 163 | public function checkoutCharge( 164 | int $amount, 165 | string $name, 166 | int $quantity = 1, 167 | array $sessionOptions = [], 168 | array $customerOptions = [], 169 | array $productData = [] 170 | ): Checkout { 171 | return $this->checkout([[ 172 | 'price_data' => [ 173 | 'currency' => $this->preferredCurrency(), 174 | 'product_data' => array_merge($productData, [ 175 | 'name' => $name, 176 | ]), 177 | 'unit_amount_decimal' => $amount, 178 | ], 179 | 'quantity' => $quantity, 180 | ]], $sessionOptions, $customerOptions); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Cashier.php: -------------------------------------------------------------------------------- 1 | id : $stripeId; 104 | 105 | $model = static::$customerModel; 106 | 107 | $builder = in_array(SoftDeletes::class, class_uses_recursive($model)) 108 | ? $model::withTrashed() 109 | : new $model; 110 | 111 | return $stripeId ? $builder->where('stripe_id', $stripeId)->first() : null; 112 | } 113 | 114 | /** 115 | * Get the Stripe SDK client. 116 | * 117 | * @param array $options 118 | * @return \Stripe\StripeClient 119 | */ 120 | public static function stripe(array $options = []) 121 | { 122 | $config = array_merge([ 123 | 'api_key' => $options['api_key'] ?? config('cashier.secret'), 124 | 'stripe_version' => static::STRIPE_VERSION, 125 | 'api_base' => static::$apiBaseUrl, 126 | ], $options); 127 | 128 | return app(StripeClient::class, ['config' => $config]); 129 | } 130 | 131 | /** 132 | * Set the custom currency formatter. 133 | * 134 | * @param callable $callback 135 | * @return void 136 | */ 137 | public static function formatCurrencyUsing(callable $callback) 138 | { 139 | static::$formatCurrencyUsing = $callback; 140 | } 141 | 142 | /** 143 | * Format the given amount into a displayable currency. 144 | * 145 | * @param int $amount 146 | * @param string|null $currency 147 | * @param string|null $locale 148 | * @param array $options 149 | * @return string 150 | */ 151 | public static function formatAmount(int $amount, ?string $currency = null, ?string $locale = null, array $options = []): string 152 | { 153 | if (static::$formatCurrencyUsing) { 154 | return call_user_func(static::$formatCurrencyUsing, $amount, $currency, $locale, $options); 155 | } 156 | 157 | $money = new Money($amount, new Currency(strtoupper($currency ?? config('cashier.currency')))); 158 | 159 | $locale = $locale ?? config('cashier.currency_locale'); 160 | 161 | $numberFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); 162 | 163 | if (isset($options['min_fraction_digits'])) { 164 | $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['min_fraction_digits']); 165 | } 166 | 167 | $moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies()); 168 | 169 | return $moneyFormatter->format($money); 170 | } 171 | 172 | /** 173 | * Configure Cashier to not register its routes. 174 | * 175 | * @return static 176 | */ 177 | public static function ignoreRoutes() 178 | { 179 | static::$registersRoutes = false; 180 | 181 | return new static; 182 | } 183 | 184 | /** 185 | * Configure Cashier to maintain past due subscriptions as active. 186 | * 187 | * @return static 188 | */ 189 | public static function keepPastDueSubscriptionsActive() 190 | { 191 | static::$deactivatePastDue = false; 192 | 193 | return new static; 194 | } 195 | 196 | /** 197 | * Configure Cashier to maintain incomplete subscriptions as active. 198 | * 199 | * @return static 200 | */ 201 | public static function keepIncompleteSubscriptionsActive() 202 | { 203 | static::$deactivateIncomplete = false; 204 | 205 | return new static; 206 | } 207 | 208 | /** 209 | * Configure Cashier to automatically calculate taxes using Stripe Tax. 210 | * 211 | * @return static 212 | */ 213 | public static function calculateTaxes() 214 | { 215 | static::$calculatesTaxes = true; 216 | 217 | return new static; 218 | } 219 | 220 | /** 221 | * Set the customer model class name. 222 | * 223 | * @param class-string<\Illuminate\Database\Eloquent\Model> $customerModel 224 | * @return void 225 | */ 226 | public static function useCustomerModel(string $customerModel): void 227 | { 228 | static::$customerModel = $customerModel; 229 | } 230 | 231 | /** 232 | * Set the subscription model class name. 233 | * 234 | * @param class-string<\Illuminate\Database\Eloquent\Model> $subscriptionModel 235 | * @return void 236 | */ 237 | public static function useSubscriptionModel(string $subscriptionModel): void 238 | { 239 | static::$subscriptionModel = $subscriptionModel; 240 | } 241 | 242 | /** 243 | * Set the subscription item model class name. 244 | * 245 | * @param class-string<\Illuminate\Database\Eloquent\Model> $subscriptionItemModel 246 | * @return void 247 | */ 248 | public static function useSubscriptionItemModel(string $subscriptionItemModel): void 249 | { 250 | static::$subscriptionItemModel = $subscriptionItemModel; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Payment.php: -------------------------------------------------------------------------------- 1 | rawAmount(), $this->paymentIntent->currency); 42 | } 43 | 44 | /** 45 | * Get the raw total amount that will be paid. 46 | * 47 | * @return int 48 | */ 49 | public function rawAmount(): int 50 | { 51 | return $this->paymentIntent->amount; 52 | } 53 | 54 | /** 55 | * The Stripe PaymentIntent client secret. 56 | * 57 | * @return string 58 | */ 59 | public function clientSecret(): string 60 | { 61 | return $this->paymentIntent->client_secret; 62 | } 63 | 64 | /** 65 | * Capture a payment that is being held for the customer. 66 | * 67 | * @param array $options 68 | * @return \Stripe\PaymentIntent 69 | */ 70 | public function capture(array $options = []) 71 | { 72 | return $this->paymentIntent->capture($options); 73 | } 74 | 75 | /** 76 | * Determine if the payment needs a valid payment method. 77 | * 78 | * @return bool 79 | */ 80 | public function requiresPaymentMethod(): bool 81 | { 82 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD; 83 | } 84 | 85 | /** 86 | * Determine if the payment needs an extra action like 3D Secure. 87 | * 88 | * @return bool 89 | */ 90 | public function requiresAction(): bool 91 | { 92 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_ACTION; 93 | } 94 | 95 | /** 96 | * Determine if the payment needs to be confirmed. 97 | * 98 | * @return bool 99 | */ 100 | public function requiresConfirmation(): bool 101 | { 102 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_CONFIRMATION; 103 | } 104 | 105 | /** 106 | * Determine if the payment needs to be captured. 107 | * 108 | * @return bool 109 | */ 110 | public function requiresCapture(): bool 111 | { 112 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_REQUIRES_CAPTURE; 113 | } 114 | 115 | /** 116 | * Cancel the payment. 117 | * 118 | * @param array $options 119 | * @return \Stripe\PaymentIntent 120 | */ 121 | public function cancel(array $options = []) 122 | { 123 | return $this->paymentIntent->cancel($options); 124 | } 125 | 126 | /** 127 | * Determine if the payment was canceled. 128 | * 129 | * @return bool 130 | */ 131 | public function isCanceled(): bool 132 | { 133 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_CANCELED; 134 | } 135 | 136 | /** 137 | * Determine if the payment was successful. 138 | * 139 | * @return bool 140 | */ 141 | public function isSucceeded(): bool 142 | { 143 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_SUCCEEDED; 144 | } 145 | 146 | /** 147 | * Determine if the payment is processing. 148 | * 149 | * @return bool 150 | */ 151 | public function isProcessing(): bool 152 | { 153 | return $this->paymentIntent->status === StripePaymentIntent::STATUS_PROCESSING; 154 | } 155 | 156 | /** 157 | * Validate if the payment intent was successful and throw an exception if not. 158 | * 159 | * @return void 160 | * 161 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 162 | */ 163 | public function validate(): void 164 | { 165 | if ($this->requiresPaymentMethod()) { 166 | throw IncompletePayment::paymentMethodRequired($this); 167 | } elseif ($this->requiresAction()) { 168 | throw IncompletePayment::requiresAction($this); 169 | } elseif ($this->requiresConfirmation()) { 170 | throw IncompletePayment::requiresConfirmation($this); 171 | } 172 | } 173 | 174 | /** 175 | * Retrieve the related customer for the payment intent if one exists. 176 | * 177 | * @return \Laravel\Cashier\Billable|null 178 | */ 179 | public function customer() 180 | { 181 | if ($this->customer) { 182 | return $this->customer; 183 | } 184 | 185 | return $this->customer = Cashier::findBillable($this->paymentIntent->customer); 186 | } 187 | 188 | /** 189 | * The Stripe PaymentIntent instance. 190 | * 191 | * @param array $expand 192 | * @return \Stripe\PaymentIntent 193 | */ 194 | public function asStripePaymentIntent(array $expand = []) 195 | { 196 | if ($expand) { 197 | return $this->customer()->stripe()->paymentIntents->retrieve( 198 | $this->paymentIntent->id, ['expand' => $expand] 199 | ); 200 | } 201 | 202 | return $this->paymentIntent; 203 | } 204 | 205 | /** 206 | * Refresh the PaymentIntent instance from the Stripe API. 207 | * 208 | * @param array $expand 209 | * @return $this 210 | */ 211 | public function refresh(array $expand = []) 212 | { 213 | $this->paymentIntent = $this->asStripePaymentIntent($expand); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Get the instance as an array. 220 | * 221 | * @return array 222 | */ 223 | public function toArray() 224 | { 225 | return $this->asStripePaymentIntent()->toArray(); 226 | } 227 | 228 | /** 229 | * Convert the object to its JSON representation. 230 | * 231 | * @param int $options 232 | * @return string 233 | */ 234 | public function toJson($options = 0) 235 | { 236 | return json_encode($this->jsonSerialize(), $options); 237 | } 238 | 239 | /** 240 | * Convert the object into something JSON serializable. 241 | * 242 | * @return array 243 | */ 244 | #[\ReturnTypeWillChange] 245 | public function jsonSerialize() 246 | { 247 | return $this->toArray(); 248 | } 249 | 250 | /** 251 | * Dynamically get values from the Stripe object. 252 | * 253 | * @param string $key 254 | * @return mixed 255 | */ 256 | public function __get($key) 257 | { 258 | return $this->paymentIntent->{$key}; 259 | } 260 | 261 | /** 262 | * Dynamically pass missing methods to the PaymentIntent instance. 263 | * 264 | * @param string $method 265 | * @param array $parameters 266 | * @return mixed 267 | */ 268 | public function __call($method, $parameters) 269 | { 270 | return $this->forwardCallTo($this->paymentIntent, $method, $parameters); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Concerns/ManagesSubscriptions.php: -------------------------------------------------------------------------------- 1 | onGenericTrial()) { 36 | return true; 37 | } 38 | 39 | $subscription = $this->subscription($type); 40 | 41 | if (! $subscription || ! $subscription->onTrial()) { 42 | return false; 43 | } 44 | 45 | return ! $price || $subscription->hasPrice($price); 46 | } 47 | 48 | /** 49 | * Determine if the Stripe model's trial has ended. 50 | * 51 | * @param string $type 52 | * @param string|null $price 53 | * @return bool 54 | */ 55 | public function hasExpiredTrial(string $type = 'default', ?string $price = null): bool 56 | { 57 | if (func_num_args() === 0 && $this->hasExpiredGenericTrial()) { 58 | return true; 59 | } 60 | 61 | $subscription = $this->subscription($type); 62 | 63 | if (! $subscription || ! $subscription->hasExpiredTrial()) { 64 | return false; 65 | } 66 | 67 | return ! $price || $subscription->hasPrice($price); 68 | } 69 | 70 | /** 71 | * Determine if the Stripe model is on a "generic" trial at the model level. 72 | * 73 | * @return bool 74 | */ 75 | public function onGenericTrial(): bool 76 | { 77 | return $this->trial_ends_at && $this->trial_ends_at->isFuture(); 78 | } 79 | 80 | /** 81 | * Filter the given query for generic trials. 82 | * 83 | * @param \Illuminate\Contracts\Database\Eloquent\Builder $query 84 | * @return void 85 | */ 86 | public function scopeOnGenericTrial(Builder $query): void 87 | { 88 | $query->whereNotNull('trial_ends_at')->where('trial_ends_at', '>', Carbon::now()); 89 | } 90 | 91 | /** 92 | * Determine if the Stripe model's "generic" trial at the model level has expired. 93 | * 94 | * @return bool 95 | */ 96 | public function hasExpiredGenericTrial(): bool 97 | { 98 | return $this->trial_ends_at && $this->trial_ends_at->isPast(); 99 | } 100 | 101 | /** 102 | * Filter the given query for expired generic trials. 103 | * 104 | * @param \Illuminate\Contracts\Database\Eloquent\Builder $query 105 | * @return void 106 | */ 107 | public function scopeHasExpiredGenericTrial(Builder $query): void 108 | { 109 | $query->whereNotNull('trial_ends_at')->where('trial_ends_at', '<', Carbon::now()); 110 | } 111 | 112 | /** 113 | * Get the ending date of the trial. 114 | * 115 | * @param string $type 116 | * @return \Illuminate\Support\Carbon|null 117 | */ 118 | public function trialEndsAt(string $type = 'default') 119 | { 120 | if (func_num_args() === 0 && $this->onGenericTrial()) { 121 | return $this->trial_ends_at; 122 | } 123 | 124 | if ($subscription = $this->subscription($type)) { 125 | return $subscription->trial_ends_at; 126 | } 127 | 128 | return $this->trial_ends_at; 129 | } 130 | 131 | /** 132 | * Determine if the Stripe model has a given subscription. 133 | * 134 | * @param string $type 135 | * @param string|null $price 136 | * @return bool 137 | */ 138 | public function subscribed(string $type = 'default', ?string $price = null): bool 139 | { 140 | $subscription = $this->subscription($type); 141 | 142 | if (! $subscription || ! $subscription->valid()) { 143 | return false; 144 | } 145 | 146 | return ! $price || $subscription->hasPrice($price); 147 | } 148 | 149 | /** 150 | * Get a subscription instance by $type. 151 | * 152 | * @param string $type 153 | * @return \Laravel\Cashier\Subscription|null 154 | */ 155 | public function subscription(string $type = 'default'): ?Subscription 156 | { 157 | return $this->subscriptions->where('type', $type)->first(); 158 | } 159 | 160 | /** 161 | * Get all of the subscriptions for the Stripe model. 162 | * 163 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 164 | */ 165 | public function subscriptions(): HasMany 166 | { 167 | return $this->hasMany(Cashier::$subscriptionModel, $this->getForeignKey())->orderBy('created_at', 'desc'); 168 | } 169 | 170 | /** 171 | * Determine if the customer's subscription has an incomplete payment. 172 | * 173 | * @param string $type 174 | * @return bool 175 | */ 176 | public function hasIncompletePayment(string $type = 'default'): bool 177 | { 178 | if ($subscription = $this->subscription($type)) { 179 | return $subscription->hasIncompletePayment(); 180 | } 181 | 182 | return false; 183 | } 184 | 185 | /** 186 | * Determine if the Stripe model is actively subscribed to one of the given products. 187 | * 188 | * @param string|string[] $products 189 | * @param string $type 190 | * @return bool 191 | */ 192 | public function subscribedToProduct(string|array $products, string $type = 'default'): bool 193 | { 194 | $subscription = $this->subscription($type); 195 | 196 | if (! $subscription || ! $subscription->valid()) { 197 | return false; 198 | } 199 | 200 | foreach ((array) $products as $product) { 201 | if ($subscription->hasProduct($product)) { 202 | return true; 203 | } 204 | } 205 | 206 | return false; 207 | } 208 | 209 | /** 210 | * Determine if the Stripe model is actively subscribed to one of the given prices. 211 | * 212 | * @param string|string[] $prices 213 | * @param string $type 214 | * @return bool 215 | */ 216 | public function subscribedToPrice(string|array $prices, $type = 'default'): bool 217 | { 218 | $subscription = $this->subscription($type); 219 | 220 | if (! $subscription || ! $subscription->valid()) { 221 | return false; 222 | } 223 | 224 | foreach ((array) $prices as $price) { 225 | if ($subscription->hasPrice($price)) { 226 | return true; 227 | } 228 | } 229 | 230 | return false; 231 | } 232 | 233 | /** 234 | * Determine if the customer has a valid subscription on the given product. 235 | * 236 | * @param string $product 237 | * @return bool 238 | */ 239 | public function onProduct(string $product): bool 240 | { 241 | return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($product) { 242 | return $subscription->valid() && $subscription->hasProduct($product); 243 | })); 244 | } 245 | 246 | /** 247 | * Determine if the customer has a valid subscription on the given price. 248 | * 249 | * @param string $price 250 | * @return bool 251 | */ 252 | public function onPrice(string $price): bool 253 | { 254 | return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($price) { 255 | return $subscription->valid() && $subscription->hasPrice($price); 256 | })); 257 | } 258 | 259 | /** 260 | * Get the tax rates to apply to the subscription. 261 | * 262 | * @return array 263 | */ 264 | public function taxRates(): array 265 | { 266 | return []; 267 | } 268 | 269 | /** 270 | * Get the tax rates to apply to individual subscription items. 271 | * 272 | * @return array 273 | */ 274 | public function priceTaxRates(): array 275 | { 276 | return []; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /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(): bool 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(?string $type = null): bool 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 69 | */ 70 | public function paymentMethods(?string $type = null, array $parameters = []): Collection 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(StripePaymentMethod|string $paymentMethod): 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(StripePaymentMethod|string $paymentMethod): void 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 null; 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(StripePaymentMethod|string $paymentMethod): PaymentMethod 169 | { 170 | $this->assertCustomerExists(); 171 | 172 | $customer = $this->asStripeCustomer(); 173 | 174 | $stripePaymentMethod = $paymentMethod instanceof StripePaymentMethod 175 | ? $paymentMethod 176 | : $this->resolveStripePaymentMethod($paymentMethod); 177 | 178 | // If the customer already has the payment method as their default, we can bail out 179 | // of the call now. We don't need to keep adding the same payment method to this 180 | // model's account every single time we go through this specific process call. 181 | if ($stripePaymentMethod->id === $customer->invoice_settings->default_payment_method) { 182 | return new PaymentMethod($this, $stripePaymentMethod); 183 | } 184 | 185 | $paymentMethod = $this->addPaymentMethod($stripePaymentMethod); 186 | 187 | $this->updateStripeCustomer([ 188 | 'invoice_settings' => ['default_payment_method' => $paymentMethod->id], 189 | ]); 190 | 191 | // Next we will get the default payment method for this user so we can update the 192 | // payment method details on the record in the database. This will allow us to 193 | // show that information on the front-end when updating the payment methods. 194 | $this->fillPaymentMethodDetails($paymentMethod); 195 | 196 | $this->save(); 197 | 198 | return $paymentMethod; 199 | } 200 | 201 | /** 202 | * Synchronises the customer's default payment method from Stripe back into the database. 203 | * 204 | * @return $this 205 | */ 206 | public function updateDefaultPaymentMethodFromStripe() 207 | { 208 | $defaultPaymentMethod = $this->defaultPaymentMethod(); 209 | 210 | if ($defaultPaymentMethod) { 211 | if ($defaultPaymentMethod instanceof PaymentMethod) { 212 | $this->fillPaymentMethodDetails( 213 | $defaultPaymentMethod->asStripePaymentMethod() 214 | )->save(); 215 | } else { 216 | $this->fillSourceDetails($defaultPaymentMethod)->save(); 217 | } 218 | } else { 219 | $this->forceFill([ 220 | 'pm_type' => null, 221 | 'pm_last_four' => null, 222 | ])->save(); 223 | } 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Fills the model's properties with the payment method from Stripe. 230 | * 231 | * @param \Laravel\Cashier\PaymentMethod|\Stripe\PaymentMethod|null $paymentMethod 232 | * @return $this 233 | */ 234 | protected function fillPaymentMethodDetails(PaymentMethod|StripePaymentMethod|null $paymentMethod) 235 | { 236 | if ($paymentMethod->type === 'card') { 237 | $this->pm_type = $paymentMethod->card->brand; 238 | $this->pm_last_four = $paymentMethod->card->last4; 239 | } else { 240 | $this->pm_type = $type = $paymentMethod->type; 241 | $this->pm_last_four = $paymentMethod?->$type->last4 ?? null; 242 | } 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Fills the model's properties with the source from Stripe. 249 | * 250 | * @param \Stripe\Card|\Stripe\BankAccount|null $source 251 | * @return $this 252 | * 253 | * @deprecated Will be removed in a future Cashier update. You should use the new payment methods API instead. 254 | */ 255 | #[\Deprecated('Will be removed in a future Cashier update. You should use the new payment methods API instead')] 256 | protected function fillSourceDetails(StripeCard|StripeBankAccount|null $source) 257 | { 258 | if ($source instanceof StripeCard) { 259 | $this->pm_type = $source->brand; 260 | $this->pm_last_four = $source->last4; 261 | } elseif ($source instanceof StripeBankAccount) { 262 | $this->pm_type = 'Bank Account'; 263 | $this->pm_last_four = $source->last4; 264 | } 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Deletes the customer's payment methods of the given type. 271 | * 272 | * @param string|null $type 273 | * @return void 274 | */ 275 | public function deletePaymentMethods($type = null) 276 | { 277 | $this->paymentMethods($type)->each(function (PaymentMethod $paymentMethod) { 278 | $paymentMethod->delete(); 279 | }); 280 | 281 | $this->updateDefaultPaymentMethodFromStripe(); 282 | } 283 | 284 | /** 285 | * Find a PaymentMethod by ID. 286 | * 287 | * @param \Stripe\PaymentMethod|string $paymentMethod 288 | * @return \Laravel\Cashier\PaymentMethod|null 289 | */ 290 | public function findPaymentMethod(StripePaymentMethod|string $paymentMethod): ?PaymentMethod 291 | { 292 | $stripePaymentMethod = null; 293 | 294 | try { 295 | $stripePaymentMethod = $this->resolveStripePaymentMethod($paymentMethod); 296 | } catch (Exception $exception) { 297 | // 298 | } 299 | 300 | return $stripePaymentMethod ? new PaymentMethod($this, $stripePaymentMethod) : null; 301 | } 302 | 303 | /** 304 | * Resolve a PaymentMethod ID to a Stripe PaymentMethod object. 305 | * 306 | * @param \Stripe\PaymentMethod|string $paymentMethod 307 | * @return \Stripe\PaymentMethod 308 | */ 309 | protected function resolveStripePaymentMethod(StripePaymentMethod|string $paymentMethod) 310 | { 311 | if ($paymentMethod instanceof StripePaymentMethod) { 312 | return $paymentMethod; 313 | } 314 | 315 | return static::stripe()->paymentMethods->retrieve($paymentMethod); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /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->isPaid()) 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 | -------------------------------------------------------------------------------- /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()->updateOrCreate([ 84 | 'stripe_id' => $data['id'], 85 | ], [ 86 | 'type' => $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload), 87 | 'stripe_status' => $data['status'], 88 | 'stripe_price' => $isSinglePrice ? $firstItem['price']['id'] : null, 89 | 'quantity' => $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null, 90 | 'trial_ends_at' => $trialEndsAt, 91 | 'ends_at' => null, 92 | ]); 93 | 94 | foreach ($data['items']['data'] as $item) { 95 | $subscription->items()->updateOrCreate([ 96 | 'stripe_id' => $item['id'], 97 | ], [ 98 | 'stripe_product' => $item['price']['product'], 99 | 'stripe_price' => $item['price']['id'], 100 | 'quantity' => $item['quantity'] ?? null, 101 | ]); 102 | } 103 | } 104 | 105 | // Terminate the billable's generic trial if it exists... 106 | if (! is_null($user->trial_ends_at)) { 107 | $user->trial_ends_at = null; 108 | $user->save(); 109 | } 110 | } 111 | 112 | return $this->successMethod(); 113 | } 114 | 115 | /** 116 | * Determines the type that should be used when new subscriptions are created from the Stripe dashboard. 117 | * 118 | * @param array $payload 119 | * @return string 120 | */ 121 | protected function newSubscriptionType(array $payload) 122 | { 123 | return 'default'; 124 | } 125 | 126 | /** 127 | * Handle customer subscription updated. 128 | * 129 | * @param array $payload 130 | * @return \Symfony\Component\HttpFoundation\Response|null 131 | */ 132 | protected function handleCustomerSubscriptionUpdated(array $payload) 133 | { 134 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 135 | $data = $payload['data']['object']; 136 | 137 | $subscription = $user->subscriptions()->firstOrNew(['stripe_id' => $data['id']]); 138 | 139 | if ( 140 | isset($data['status']) && 141 | $data['status'] === StripeSubscription::STATUS_INCOMPLETE_EXPIRED 142 | ) { 143 | $subscription->items()->delete(); 144 | $subscription->delete(); 145 | 146 | return null; 147 | } 148 | 149 | $subscription->type = $subscription->type ?? $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload); 150 | 151 | $firstItem = $data['items']['data'][0]; 152 | $isSinglePrice = count($data['items']['data']) === 1; 153 | 154 | // Price... 155 | $subscription->stripe_price = $isSinglePrice ? $firstItem['price']['id'] : null; 156 | 157 | // Quantity... 158 | $subscription->quantity = $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null; 159 | 160 | // Trial ending date... 161 | if (isset($data['trial_end'])) { 162 | $trialEnd = Carbon::createFromTimestamp($data['trial_end']); 163 | 164 | if (! $subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) { 165 | $subscription->trial_ends_at = $trialEnd; 166 | } 167 | } 168 | 169 | // Cancellation date... 170 | if ($data['cancel_at_period_end'] ?? false) { 171 | $subscription->ends_at = $subscription->onTrial() 172 | ? $subscription->trial_ends_at 173 | : $subscription->currentPeriodEnd(); 174 | } elseif (isset($data['cancel_at']) || isset($data['canceled_at'])) { 175 | $subscription->ends_at = Carbon::createFromTimestamp($data['cancel_at'] ?? $data['canceled_at']); 176 | } else { 177 | $subscription->ends_at = null; 178 | } 179 | 180 | // Status... 181 | if (isset($data['status'])) { 182 | $subscription->stripe_status = $data['status']; 183 | } 184 | 185 | $subscription->save(); 186 | 187 | // Update subscription items... 188 | if (isset($data['items'])) { 189 | $subscriptionItemIds = []; 190 | 191 | foreach ($data['items']['data'] as $item) { 192 | $subscriptionItemIds[] = $item['id']; 193 | 194 | $subscription->items()->updateOrCreate([ 195 | 'stripe_id' => $item['id'], 196 | ], [ 197 | 'stripe_product' => $item['price']['product'], 198 | 'stripe_price' => $item['price']['id'], 199 | 'quantity' => $item['quantity'] ?? null, 200 | ]); 201 | } 202 | 203 | // Delete items that aren't attached to the subscription anymore... 204 | $subscription->items()->whereNotIn('stripe_id', $subscriptionItemIds)->delete(); 205 | } 206 | } 207 | 208 | return $this->successMethod(); 209 | } 210 | 211 | /** 212 | * Handle the cancellation of a customer subscription. 213 | * 214 | * @param array $payload 215 | * @return \Symfony\Component\HttpFoundation\Response 216 | */ 217 | protected function handleCustomerSubscriptionDeleted(array $payload) 218 | { 219 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 220 | $user->subscriptions->filter(function ($subscription) use ($payload) { 221 | return $subscription->stripe_id === $payload['data']['object']['id']; 222 | })->each(function ($subscription) { 223 | $subscription->skipTrial()->markAsCanceled(); 224 | }); 225 | } 226 | 227 | return $this->successMethod(); 228 | } 229 | 230 | /** 231 | * Handle customer updated. 232 | * 233 | * @param array $payload 234 | * @return \Symfony\Component\HttpFoundation\Response 235 | */ 236 | protected function handleCustomerUpdated(array $payload) 237 | { 238 | if ($user = $this->getUserByStripeId($payload['data']['object']['id'])) { 239 | $user->updateDefaultPaymentMethodFromStripe(); 240 | } 241 | 242 | return $this->successMethod(); 243 | } 244 | 245 | /** 246 | * Handle deleted customer. 247 | * 248 | * @param array $payload 249 | * @return \Symfony\Component\HttpFoundation\Response 250 | */ 251 | protected function handleCustomerDeleted(array $payload) 252 | { 253 | if ($user = $this->getUserByStripeId($payload['data']['object']['id'])) { 254 | $user->subscriptions->each(function (Subscription $subscription) { 255 | $subscription->skipTrial()->markAsCanceled(); 256 | }); 257 | 258 | $user->forceFill([ 259 | 'stripe_id' => null, 260 | 'trial_ends_at' => null, 261 | 'pm_type' => null, 262 | 'pm_last_four' => null, 263 | ])->save(); 264 | } 265 | 266 | return $this->successMethod(); 267 | } 268 | 269 | /** 270 | * Handle payment method automatically updated by vendor. 271 | * 272 | * @param array $payload 273 | * @return \Symfony\Component\HttpFoundation\Response 274 | */ 275 | protected function handlePaymentMethodAutomaticallyUpdated(array $payload) 276 | { 277 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 278 | $user->updateDefaultPaymentMethodFromStripe(); 279 | } 280 | 281 | return $this->successMethod(); 282 | } 283 | 284 | /** 285 | * Handle payment action required for invoice. 286 | * 287 | * @param array $payload 288 | * @return \Symfony\Component\HttpFoundation\Response 289 | */ 290 | protected function handleInvoicePaymentActionRequired(array $payload) 291 | { 292 | if (is_null($notification = config('cashier.payment_notification'))) { 293 | return $this->successMethod(); 294 | } 295 | 296 | if ($payload['data']['object']['metadata']['is_on_session_checkout'] ?? false) { 297 | return $this->successMethod(); 298 | } 299 | 300 | if ($payload['data']['object']['subscription_details']['metadata']['is_on_session_checkout'] ?? false) { 301 | return $this->successMethod(); 302 | } 303 | 304 | if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) { 305 | if (in_array(Notifiable::class, class_uses_recursive($user))) { 306 | if (isset($payload['data']['object']['payment_intent'])) { 307 | $paymentIntent = $user->stripe()->paymentIntents->retrieve( 308 | $payload['data']['object']['payment_intent'] 309 | ); 310 | 311 | $payment = new Payment($paymentIntent); 312 | 313 | $user->notify(new $notification($payment)); 314 | } 315 | } 316 | } 317 | 318 | return $this->successMethod(); 319 | } 320 | 321 | /** 322 | * Get the customer instance by Stripe ID. 323 | * 324 | * @param string|null $stripeId 325 | * @return \Laravel\Cashier\Billable|null 326 | */ 327 | protected function getUserByStripeId($stripeId) 328 | { 329 | return Cashier::findBillable($stripeId); 330 | } 331 | 332 | /** 333 | * Handle successful calls on the controller. 334 | * 335 | * @param array $parameters 336 | * @return \Symfony\Component\HttpFoundation\Response 337 | */ 338 | protected function successMethod($parameters = []) 339 | { 340 | return new Response('Webhook Handled', 200); 341 | } 342 | 343 | /** 344 | * Handle calls to missing methods on the controller. 345 | * 346 | * @param array $parameters 347 | * @return \Symfony\Component\HttpFoundation\Response 348 | */ 349 | protected function missingMethod($parameters = []) 350 | { 351 | return new Response; 352 | } 353 | 354 | /** 355 | * Set the number of automatic retries due to an object lock timeout from Stripe. 356 | * 357 | * @param int $retries 358 | * @return void 359 | */ 360 | protected function setMaxNetworkRetries($retries = 3) 361 | { 362 | Stripe::setMaxNetworkRetries($retries); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/SubscriptionItem.php: -------------------------------------------------------------------------------- 1 | 'integer', 40 | 'meter_id' => 'string', 41 | 'meter_event_name' => 'string', 42 | ]; 43 | 44 | /** 45 | * Get the subscription that the item belongs to. 46 | * 47 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 48 | */ 49 | public function subscription(): BelongsTo 50 | { 51 | $model = Cashier::$subscriptionModel; 52 | 53 | return $this->belongsTo($model, (new $model)->getForeignKey()); 54 | } 55 | 56 | /** 57 | * Increment the quantity of the subscription item. 58 | * 59 | * @param int $count 60 | * @return $this 61 | * 62 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 63 | */ 64 | public function incrementQuantity(int $count = 1) 65 | { 66 | $this->updateQuantity($this->quantity + $count); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Increment the quantity of the subscription item, and invoice immediately. 73 | * 74 | * @param int $count 75 | * @return $this 76 | * 77 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 78 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 79 | */ 80 | public function incrementAndInvoice(int $count = 1) 81 | { 82 | $this->alwaysInvoice(); 83 | 84 | $this->incrementQuantity($count); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Decrement the quantity of the subscription item. 91 | * 92 | * @param int $count 93 | * @return $this 94 | * 95 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 96 | */ 97 | public function decrementQuantity(int $count = 1) 98 | { 99 | $this->updateQuantity(max(1, $this->quantity - $count)); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Update the quantity of the subscription item. 106 | * 107 | * @param int $quantity 108 | * @return $this 109 | * 110 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 111 | */ 112 | public function updateQuantity(int $quantity) 113 | { 114 | $this->subscription->guardAgainstIncomplete(); 115 | 116 | $stripeSubscriptionItem = $this->updateStripeSubscriptionItem([ 117 | 'payment_behavior' => $this->paymentBehavior(), 118 | 'proration_behavior' => $this->prorateBehavior(), 119 | 'quantity' => $quantity, 120 | ]); 121 | 122 | $this->fill([ 123 | 'quantity' => $stripeSubscriptionItem->quantity, 124 | ])->save(); 125 | 126 | $stripeSubscription = $this->subscription->asStripeSubscription(); 127 | 128 | if ($this->subscription->hasSinglePrice()) { 129 | $this->subscription->fill([ 130 | 'quantity' => $stripeSubscriptionItem->quantity, 131 | ]); 132 | } 133 | 134 | $this->subscription->fill([ 135 | 'stripe_status' => $stripeSubscription->status, 136 | ])->save(); 137 | 138 | $this->handlePaymentFailure($this->subscription); 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Swap the subscription item to a new Stripe price. 145 | * 146 | * @param string $price 147 | * @param array $options 148 | * @return $this 149 | * 150 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 151 | */ 152 | public function swap(string $price, array $options = []) 153 | { 154 | $this->subscription->guardAgainstIncomplete(); 155 | 156 | $stripePrice = $this->subscription->owner->stripe()->prices->retrieve($price); 157 | 158 | $meterId = null; 159 | $meterEventName = null; 160 | 161 | if (isset($stripePrice->recurring->meter)) { 162 | $meterId = $stripePrice->recurring->meter; 163 | $meter = $this->subscription->owner->stripe()->billing->meters->retrieve($meterId); 164 | $meterEventName = $meter->event_name; 165 | } 166 | 167 | $stripeSubscriptionItem = $this->updateStripeSubscriptionItem(array_merge( 168 | array_filter([ 169 | 'price' => $price, 170 | 'quantity' => $this->quantity, 171 | 'payment_behavior' => $this->paymentBehavior(), 172 | 'proration_behavior' => $this->prorateBehavior(), 173 | 'tax_rates' => $this->subscription->getPriceTaxRatesForPayload($price), 174 | ], function ($value) { 175 | return ! is_null($value); 176 | }), 177 | $options)); 178 | 179 | $this->fill([ 180 | 'stripe_product' => $stripeSubscriptionItem->price->product, 181 | 'stripe_price' => $stripeSubscriptionItem->price->id, 182 | 'meter_id' => $meterId, 183 | 'quantity' => $stripeSubscriptionItem->quantity, 184 | 'meter_event_name' => $meterEventName, 185 | ])->save(); 186 | 187 | $stripeSubscription = $this->subscription->asStripeSubscription(); 188 | 189 | if ($this->subscription->hasSinglePrice()) { 190 | $this->subscription->fill([ 191 | 'stripe_price' => $price, 192 | 'quantity' => $stripeSubscriptionItem->quantity, 193 | ]); 194 | } 195 | 196 | $this->subscription->fill([ 197 | 'stripe_status' => $stripeSubscription->status, 198 | ])->save(); 199 | 200 | $this->handlePaymentFailure($this->subscription); 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Swap the subscription item to a new Stripe price, and invoice immediately. 207 | * 208 | * @param string $price 209 | * @param array $options 210 | * @return $this 211 | * 212 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 213 | * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure 214 | */ 215 | public function swapAndInvoice(string $price, array $options = []) 216 | { 217 | $this->alwaysInvoice(); 218 | 219 | return $this->swap($price, $options); 220 | } 221 | 222 | /** 223 | * Report usage for a metered product. 224 | * 225 | * @param int $quantity 226 | * @param \DateTimeInterface|int|null $timestamp 227 | * @return \Stripe\V2\Billing\MeterEvent 228 | */ 229 | public function reportUsage(int $quantity = 1, DateTimeInterface|int|null $timestamp = null) 230 | { 231 | $eventName = $this->meter_event_name; 232 | $meterId = $this->meter_id; 233 | 234 | if (! $eventName) { 235 | if (! $meterId) { 236 | // Get the price to determine the meter... 237 | $stripePrice = $this->subscription->owner->stripe()->prices->retrieve($this->stripe_price); 238 | 239 | if (! isset($stripePrice->recurring->meter)) { 240 | throw new \InvalidArgumentException('Price must have a meter to report usage. Legacy usage records are no longer supported.'); 241 | } 242 | 243 | $meterId = $stripePrice->recurring->meter; 244 | } 245 | 246 | // Get the meter to get the event name... 247 | $meter = $this->subscription->owner->stripe()->billing->meters->retrieve($meterId); 248 | 249 | $eventName = $meter->event_name; 250 | 251 | $this->forceFill(['meter_id' => $meterId, 'meter_event_name' => $eventName])->save(); 252 | } 253 | 254 | // Convert timestamp to RFC 3339 format for v2 API... 255 | if ($timestamp instanceof DateTimeInterface) { 256 | $rfc3339Timestamp = $timestamp->format('c'); 257 | } elseif (is_int($timestamp)) { 258 | $rfc3339Timestamp = (new \DateTime('@'.$timestamp))->format('c'); 259 | } else { 260 | $rfc3339Timestamp = (new \DateTime())->format('c'); 261 | } 262 | 263 | return $this->subscription->owner->stripe()->v2->billing->meterEvents->create([ 264 | 'event_name' => $eventName, 265 | 'payload' => [ 266 | 'stripe_customer_id' => $this->subscription->owner->stripeId(), 267 | 'value' => (string) $quantity, 268 | ], 269 | 'timestamp' => $rfc3339Timestamp, 270 | 'identifier' => Str::uuid()->toString(), 271 | ]); 272 | } 273 | 274 | /** 275 | * Get the usage records for a metered product. 276 | * 277 | * @param array $options 278 | * @return \Illuminate\Support\Collection 279 | */ 280 | public function usageRecords(array $options = []): Collection 281 | { 282 | $meterId = $this->meter_id; 283 | 284 | if (! $meterId) { 285 | // Get the price to determine the meter... 286 | $stripePrice = $this->subscription->owner->stripe()->prices->retrieve($this->stripe_price); 287 | 288 | if (! isset($stripePrice->recurring->meter)) { 289 | throw new \InvalidArgumentException('Price must have a meter to get usage records. Legacy usage records are no longer supported.'); 290 | } 291 | 292 | $meterId = $stripePrice->recurring->meter; 293 | 294 | $this->forceFill(['meter_id' => $meterId])->save(); 295 | } 296 | 297 | // Default time range - current billing period... 298 | $defaultOptions = [ 299 | 'start_time' => $this->currentPeriodStart()?->getTimestamp() ?? 1, 300 | 'end_time' => time(), 301 | 'customer' => $this->subscription->owner->stripeId(), 302 | ]; 303 | 304 | return new Collection($this->subscription->owner->stripe()->billing->meters->allEventSummaries( 305 | $meterId, 306 | array_merge($defaultOptions, $options) 307 | )->data); 308 | } 309 | 310 | /** 311 | * Get the current period start date for this subscription item. 312 | * 313 | * @param string|null $timezone 314 | * @return \Illuminate\Support\Carbon|null 315 | */ 316 | public function currentPeriodStart(?string $timezone = null) 317 | { 318 | $stripeItem = $this->asStripeSubscriptionItem(); 319 | 320 | if (! isset($stripeItem->current_period_start)) { 321 | return null; 322 | } 323 | 324 | $date = $this->asDateTime($stripeItem->current_period_start); 325 | 326 | return $timezone ? $date->setTimezone($timezone) : $date; 327 | } 328 | 329 | /** 330 | * Get the current period end date for this subscription item. 331 | * 332 | * @param string|null $timezone 333 | * @return \Illuminate\Support\Carbon|null 334 | */ 335 | public function currentPeriodEnd(?string $timezone = null) 336 | { 337 | $stripeItem = $this->asStripeSubscriptionItem(); 338 | 339 | if (! isset($stripeItem->current_period_end)) { 340 | return null; 341 | } 342 | 343 | $date = $this->asDateTime($stripeItem->current_period_end); 344 | 345 | return $timezone ? $date->setTimezone($timezone) : $date; 346 | } 347 | 348 | /** 349 | * Determine if the subscription item is currently within its trial period. 350 | * 351 | * @return bool 352 | */ 353 | public function onTrial(): bool 354 | { 355 | return $this->subscription->onTrial(); 356 | } 357 | 358 | /** 359 | * Determine if the subscription item is on a grace period after cancellation. 360 | * 361 | * @return bool 362 | */ 363 | public function onGracePeriod(): bool 364 | { 365 | return $this->subscription->onGracePeriod(); 366 | } 367 | 368 | /** 369 | * Update the underlying Stripe subscription item information for the model. 370 | * 371 | * @param array $options 372 | * @return \Stripe\SubscriptionItem 373 | */ 374 | public function updateStripeSubscriptionItem(array $options = []) 375 | { 376 | return $this->subscription->owner->stripe()->subscriptionItems->update( 377 | $this->stripe_id, $options 378 | ); 379 | } 380 | 381 | /** 382 | * Get the subscription as a Stripe subscription item object. 383 | * 384 | * @param array $expand 385 | * @return \Stripe\SubscriptionItem 386 | */ 387 | public function asStripeSubscriptionItem(array $expand = []) 388 | { 389 | return $this->subscription->owner->stripe()->subscriptionItems->retrieve( 390 | $this->stripe_id, ['expand' => $expand] 391 | ); 392 | } 393 | 394 | /** 395 | * Create a new factory instance for the model. 396 | * 397 | * @return \Illuminate\Database\Eloquent\Factories\Factory 398 | */ 399 | protected static function newFactory() 400 | { 401 | return SubscriptionItemFactory::new(); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/Concerns/ManagesInvoices.php: -------------------------------------------------------------------------------- 1 | isAutomaticTaxEnabled() && ! array_key_exists('price_data', $options)) { 37 | throw new LogicException( 38 | 'When using automatic tax calculation, you must include "price_data" in the provided options array.' 39 | ); 40 | } 41 | 42 | $this->assertCustomerExists(); 43 | 44 | $options = array_merge([ 45 | 'customer' => $this->stripe_id, 46 | 'currency' => $this->preferredCurrency(), 47 | 'description' => $description, 48 | ], $options); 49 | 50 | if (array_key_exists('price_data', $options)) { 51 | $options['price_data'] = array_merge([ 52 | 'unit_amount_decimal' => $amount, 53 | 'currency' => $this->preferredCurrency(), 54 | ], $options['price_data']); 55 | } elseif (array_key_exists('quantity', $options)) { 56 | $options['unit_amount_decimal'] = $options['unit_amount_decimal'] ?? $amount; 57 | } else { 58 | $options['amount'] = $amount; 59 | } 60 | 61 | /** @var \Stripe\Service\InvoiceItemService $invoiceItems */ 62 | $invoiceItemsService = static::stripe()->invoiceItems; 63 | 64 | return $invoiceItemsService->create($options); 65 | } 66 | 67 | /** 68 | * Invoice the customer for the given amount and generate an invoice immediately. 69 | * 70 | * @param string $description 71 | * @param int $amount 72 | * @param array $tabOptions 73 | * @param array $invoiceOptions 74 | * @return \Laravel\Cashier\Invoice 75 | * 76 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 77 | */ 78 | public function invoiceFor(string $description, int $amount, array $tabOptions = [], array $invoiceOptions = []): Invoice 79 | { 80 | $this->tab($description, $amount, $tabOptions); 81 | 82 | return $this->invoice($invoiceOptions); 83 | } 84 | 85 | /** 86 | * Add an invoice item for a specific Price ID to the customer's upcoming invoice. 87 | * 88 | * @param \Stripe\Price|string $price 89 | * @param int $quantity 90 | * @param array $options 91 | * @return \Stripe\InvoiceItem 92 | */ 93 | public function tabPrice(StripePrice|string $price, int $quantity = 1, array $options = []) 94 | { 95 | $this->assertCustomerExists(); 96 | 97 | $options = array_merge([ 98 | 'customer' => $this->stripe_id, 99 | 'pricing' => ['price' => $price], 100 | 'quantity' => $quantity, 101 | ], $options); 102 | 103 | /** @var \Stripe\Service\InvoiceItemService $invoiceItems */ 104 | $invoiceItemsService = static::stripe()->invoiceItems; 105 | 106 | return $invoiceItemsService->create($options); 107 | } 108 | 109 | /** 110 | * Invoice the customer for the given Price ID and generate an invoice immediately. 111 | * 112 | * @param string $price 113 | * @param int $quantity 114 | * @param array $tabOptions 115 | * @param array $invoiceOptions 116 | * @return \Laravel\Cashier\Invoice 117 | * 118 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 119 | */ 120 | public function invoicePrice(StripePrice|string $price, int $quantity = 1, array $tabOptions = [], array $invoiceOptions = []): Invoice 121 | { 122 | $this->tabPrice($price, $quantity, $tabOptions); 123 | 124 | return $this->invoice($invoiceOptions); 125 | } 126 | 127 | /** 128 | * Invoice the customer outside of the regular billing cycle. 129 | * 130 | * @param array $options 131 | * @return \Laravel\Cashier\Invoice 132 | * 133 | * @throws \Laravel\Cashier\Exceptions\IncompletePayment 134 | */ 135 | public function invoice(array $options = []): Invoice 136 | { 137 | try { 138 | $payOptions = Arr::only($options, $payOptionKeys = [ 139 | 'forgive', 140 | 'mandate', 141 | 'off_session', 142 | 'payment_method', 143 | 'source', 144 | ]); 145 | 146 | Arr::forget($options, $payOptionKeys); 147 | 148 | $invoice = $this->createInvoice(array_merge([ 149 | 'pending_invoice_items_behavior' => 'include', 150 | ], $options)); 151 | 152 | return $invoice->chargesAutomatically() ? $invoice->pay($payOptions) : $invoice->send(); 153 | } catch (StripeCardException $exception) { 154 | // Get the latest payment from the invoice payments... 155 | $stripeInvoice = $invoice->refresh(['payments'])->asStripeInvoice(); 156 | 157 | $invoicePayments = $stripeInvoice->payments->data; 158 | 159 | if (! empty($invoicePayments)) { 160 | $latestPayment = end($invoicePayments); 161 | 162 | if ($latestPayment->payment && $latestPayment->payment->payment_intent) { 163 | $payment = new Payment( 164 | static::stripe()->paymentIntents->retrieve( 165 | $latestPayment->payment->payment_intent, 166 | ['expand' => ['invoice.subscription']] 167 | ) 168 | ); 169 | 170 | $payment->validate(); 171 | } 172 | } 173 | 174 | throw $exception; 175 | } 176 | } 177 | 178 | /** 179 | * Create an invoice within Stripe. 180 | * 181 | * @param array $options 182 | * @return \Laravel\Cashier\Invoice 183 | */ 184 | public function createInvoice(array $options = []): Invoice 185 | { 186 | $this->assertCustomerExists(); 187 | 188 | $stripeCustomer = $this->asStripeCustomer(); 189 | 190 | $parameters = array_merge([ 191 | 'automatic_tax' => $this->automaticTaxPayload(), 192 | 'customer' => $this->stripe_id, 193 | 'currency' => $stripeCustomer->currency ?? config('cashier.currency'), 194 | ], $options); 195 | 196 | if (isset($parameters['subscription'])) { 197 | unset($parameters['currency']); 198 | } 199 | 200 | if (array_key_exists('subscription', $parameters)) { 201 | unset($parameters['pending_invoice_items_behavior']); 202 | } 203 | 204 | $stripeInvoice = static::stripe()->invoices->create($parameters); 205 | 206 | return new Invoice($this, $stripeInvoice); 207 | } 208 | 209 | /** 210 | * Get the customer's upcoming invoice. 211 | * 212 | * @param array $options 213 | * @return \Laravel\Cashier\Invoice|null 214 | */ 215 | public function upcomingInvoice(array $options = []): ?Invoice 216 | { 217 | if (! $this->hasStripeId()) { 218 | return null; 219 | } 220 | 221 | $parameters = array_merge([ 222 | 'automatic_tax' => $this->automaticTaxPayload(), 223 | 'customer' => $this->stripe_id, 224 | ], $options); 225 | 226 | // For the new Create Preview Invoice API, we need to provide specific details.... 227 | if (! $this->hasRequiredPreviewDetails($parameters)) { 228 | $activeSubscription = $this->subscriptions()->active()->first(); 229 | 230 | if ($activeSubscription) { 231 | $parameters['subscription'] = $activeSubscription->stripe_id; 232 | } 233 | } 234 | 235 | try { 236 | $stripeInvoice = static::stripe()->invoices->createPreview($parameters); 237 | 238 | return new Invoice($this, $stripeInvoice, $parameters); 239 | } catch (StripeInvalidRequestException $exception) { 240 | return null; 241 | } 242 | } 243 | 244 | /** 245 | * Check if the parameters contain the required details for the Create Preview Invoice API. 246 | * 247 | * @param array $parameters 248 | * @return bool 249 | */ 250 | protected function hasRequiredPreviewDetails(array $parameters): bool 251 | { 252 | return isset($parameters['subscription']) || 253 | isset($parameters['subscription_details']) || 254 | isset($parameters['schedule']) || 255 | isset($parameters['schedule_details']) || 256 | isset($parameters['invoice_items']); 257 | } 258 | 259 | /** 260 | * Find an invoice by ID. 261 | * 262 | * @param string $id 263 | * @return \Laravel\Cashier\Invoice|null 264 | */ 265 | public function findInvoice(string $id): ?Invoice 266 | { 267 | $stripeInvoice = null; 268 | 269 | try { 270 | $stripeInvoice = static::stripe()->invoices->retrieve($id); 271 | } catch (StripeInvalidRequestException $exception) { 272 | // 273 | } 274 | 275 | return $stripeInvoice ? new Invoice($this, $stripeInvoice) : null; 276 | } 277 | 278 | /** 279 | * Find an invoice or throw a 404 or 403 error. 280 | * 281 | * @param string $id 282 | * @return \Laravel\Cashier\Invoice 283 | * 284 | * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException 285 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 286 | */ 287 | public function findInvoiceOrFail(string $id): Invoice 288 | { 289 | try { 290 | $invoice = $this->findInvoice($id); 291 | } catch (InvalidInvoice $exception) { 292 | throw new AccessDeniedHttpException; 293 | } 294 | 295 | if (is_null($invoice)) { 296 | throw new NotFoundHttpException; 297 | } 298 | 299 | return $invoice; 300 | } 301 | 302 | /** 303 | * Create an invoice download Response. 304 | * 305 | * @param string $id 306 | * @param array $data 307 | * @param string $filename 308 | * @return \Symfony\Component\HttpFoundation\Response 309 | */ 310 | public function downloadInvoice(string $id, array $data = [], ?string $filename = null): SymfonyResponse 311 | { 312 | $invoice = $this->findInvoiceOrFail($id); 313 | 314 | return $filename ? $invoice->downloadAs($filename, $data) : $invoice->download($data); 315 | } 316 | 317 | /** 318 | * Get a collection of the customer's invoices. 319 | * 320 | * @param bool $includePending 321 | * @param array $parameters 322 | * @return \Illuminate\Support\Collection 323 | */ 324 | public function invoices(bool $includePending = false, array $parameters = []): Collection 325 | { 326 | if (! $this->hasStripeId()) { 327 | return new Collection(); 328 | } 329 | 330 | $invoices = []; 331 | 332 | $parameters = array_merge(['limit' => 24], $parameters); 333 | 334 | $stripeInvoices = static::stripe()->invoices->all( 335 | ['customer' => $this->stripe_id] + $parameters 336 | ); 337 | 338 | // Here we will loop through the Stripe invoices and create our own custom Invoice 339 | // instances that have more helper methods and are generally more convenient to 340 | // work with than the plain Stripe objects are. Then, we'll return the array. 341 | if (! is_null($stripeInvoices)) { 342 | foreach ($stripeInvoices->data as $invoice) { 343 | $invoiceInstance = new Invoice($this, $invoice); 344 | 345 | if ($invoiceInstance->isPaid() || $includePending) { 346 | $invoices[] = $invoiceInstance; 347 | } 348 | } 349 | } 350 | 351 | return new Collection($invoices); 352 | } 353 | 354 | /** 355 | * Get an array of the customer's invoices, including pending invoices. 356 | * 357 | * @param array $parameters 358 | * @return \Illuminate\Support\Collection 359 | */ 360 | public function invoicesIncludingPending(array $parameters = []): Collection 361 | { 362 | return $this->invoices(true, $parameters); 363 | } 364 | 365 | /** 366 | * Get a cursor paginator for the customer's invoices. 367 | * 368 | * @param int|null $perPage 369 | * @param array $parameters 370 | * @param string $cursorName 371 | * @param \Illuminate\Pagination\Cursor|string|null $cursor 372 | * @return \Illuminate\Contracts\Pagination\CursorPaginator 373 | */ 374 | public function cursorPaginateInvoices( 375 | ?int $perPage = 24, 376 | array $parameters = [], 377 | string $cursorName = 'cursor', 378 | Cursor|string|null $cursor = null 379 | ): CursorPaginator { 380 | if (! $cursor instanceof Cursor) { 381 | $cursor = is_string($cursor) 382 | ? Cursor::fromEncoded($cursor) 383 | : CursorPaginator::resolveCurrentCursor($cursorName, $cursor); 384 | } 385 | 386 | if (! is_null($cursor)) { 387 | if ($cursor->pointsToNextItems()) { 388 | $parameters['starting_after'] = $cursor->parameter('id'); 389 | } else { 390 | $parameters['ending_before'] = $cursor->parameter('id'); 391 | } 392 | } 393 | 394 | $invoices = $this->invoices(true, array_merge($parameters, ['limit' => $perPage + 1])); 395 | 396 | if (! is_null($cursor) && $cursor->pointsToPreviousItems()) { 397 | $invoices = $invoices->reverse(); 398 | } 399 | 400 | return new CursorPaginator($invoices, $perPage, $cursor, array_merge([ 401 | 'path' => Paginator::resolveCurrentPath(), 402 | 'cursorName' => $cursorName, 403 | 'parameters' => ['id'], 404 | ])); 405 | } 406 | 407 | /** 408 | * Get invoice payments for a specific payment intent. 409 | * 410 | * @param string $paymentIntentId 411 | * @return \Illuminate\Support\Collection 412 | */ 413 | public function invoicePaymentsForPaymentIntent(string $paymentIntentId): Collection 414 | { 415 | $invoicePayments = static::stripe()->invoicePayments->all([ 416 | 'payment' => [ 417 | 'type' => 'payment_intent', 418 | 'payment_intent' => $paymentIntentId, 419 | ], 420 | ]); 421 | 422 | return collect($invoicePayments->data)->map(function ($payment) { 423 | return new InvoicePayment($payment); 424 | }); 425 | } 426 | 427 | /** 428 | * Get invoice payments for a specific invoice. 429 | * 430 | * @param string $invoiceId 431 | * @return \Illuminate\Support\Collection 432 | */ 433 | public function invoicePaymentsForInvoice(string $invoiceId): Collection 434 | { 435 | $invoicePayments = static::stripe()->invoicePayments->all([ 436 | 'invoice' => $invoiceId, 437 | ]); 438 | 439 | return collect($invoicePayments->data)->map(function ($payment) { 440 | return new InvoicePayment($payment); 441 | }); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/InvoiceLineItem.php: -------------------------------------------------------------------------------- 1 | formatAmount($this->item->amount); 38 | } 39 | 40 | /** 41 | * Get the unit amount excluding tax for the invoice line item. 42 | * 43 | * @return string 44 | */ 45 | public function unitAmountExcludingTax(): string 46 | { 47 | return $this->formatAmount($this->taxes()->sum('taxable_amount') ?? 0); 48 | } 49 | 50 | /** 51 | * Get the unit amount from the pricing structure. 52 | * 53 | * @return int|null 54 | */ 55 | public function unitAmount(): ?int 56 | { 57 | if (! isset($this->item->pricing)) { 58 | return null; 59 | } 60 | 61 | // Handle the new pricing structure (Basil release)... 62 | if (isset($this->item->pricing->unit_amount_decimal)) { 63 | return (int) $this->item->pricing->unit_amount_decimal; 64 | } 65 | 66 | // For inline price data... 67 | if ( 68 | $this->item->pricing->type === 'inline_price_data' && 69 | isset($this->item->pricing->inline_price_data->unit_amount) 70 | ) { 71 | return (int) $this->item->pricing->inline_price_data->unit_amount; 72 | } 73 | 74 | return null; 75 | } 76 | 77 | /** 78 | * Get the formatted unit amount. 79 | * 80 | * @return string 81 | */ 82 | public function formattedUnitAmount(): string 83 | { 84 | $unitAmount = $this->unitAmount(); 85 | 86 | return $unitAmount ? $this->formatAmount($unitAmount) : '$0.00'; 87 | } 88 | 89 | /** 90 | * Determine if the line item has both inclusive and exclusive tax. 91 | * 92 | * @return bool 93 | */ 94 | public function hasBothInclusiveAndExclusiveTax(): bool 95 | { 96 | return $this->inclusiveTaxPercentage() && $this->exclusiveTaxPercentage(); 97 | } 98 | 99 | /** 100 | * Get the total percentage of the default inclusive tax for the invoice line item. 101 | * 102 | * @return float|int|null 103 | */ 104 | public function inclusiveTaxPercentage(): float|int|null 105 | { 106 | if ($this->invoice->isNotTaxExempt()) { 107 | return $this->calculateTaxPercentageByTaxAmount(true); 108 | } 109 | 110 | return $this->calculateTaxPercentageByTaxRate(true); 111 | } 112 | 113 | /** 114 | * Get the total percentage of the default exclusive tax for the invoice line item. 115 | * 116 | * @return float|int 117 | */ 118 | public function exclusiveTaxPercentage(): float|int 119 | { 120 | if ($this->invoice->isNotTaxExempt()) { 121 | return $this->calculateTaxPercentageByTaxAmount(false); 122 | } 123 | 124 | return $this->calculateTaxPercentageByTaxRate(false); 125 | } 126 | 127 | /** 128 | * Calculate the total tax percentage for either the inclusive or exclusive tax by tax rate. 129 | * 130 | * @param bool $inclusive 131 | * @return float|int 132 | */ 133 | protected function calculateTaxPercentageByTaxRate(bool $inclusive): float|int 134 | { 135 | if (! isset($this->item->taxes) || empty($this->item->taxes)) { 136 | return 0; 137 | } 138 | 139 | return Collection::make($this->item->taxes) 140 | ->filter(function (object $tax) use ($inclusive) { 141 | if ($tax->type !== 'tax_rate_details' || ! isset($tax->tax_rate_details)) { 142 | return false; 143 | } 144 | 145 | $taxRate = $this->getTaxRate($tax->tax_rate_details); 146 | 147 | return $taxRate && $taxRate->inclusive === (bool) $inclusive; 148 | }) 149 | ->sum(function (object $tax) { 150 | $taxRate = $this->getTaxRate($tax->tax_rate_details); 151 | 152 | return $taxRate ? $taxRate->percentage : 0; 153 | }); 154 | } 155 | 156 | /** 157 | * Calculate the total tax percentage for either the inclusive or exclusive tax by tax amount. 158 | * 159 | * @param bool $inclusive 160 | * @return float|int 161 | */ 162 | protected function calculateTaxPercentageByTaxAmount(bool $inclusive): float|int 163 | { 164 | if (! isset($this->item->taxes) || empty($this->item->taxes)) { 165 | return 0; 166 | } 167 | 168 | return Collection::make($this->item->taxes) 169 | ->filter(function (object $tax) use ($inclusive) { 170 | if ($tax->type !== 'tax_rate_details' || ! isset($tax->tax_rate_details)) { 171 | return false; 172 | } 173 | 174 | $taxRate = $this->getTaxRate($tax->tax_rate_details); 175 | 176 | return $taxRate && $taxRate->inclusive === (bool) $inclusive; 177 | }) 178 | ->sum(function (object $tax) { 179 | $taxRate = $this->getTaxRate($tax->tax_rate_details); 180 | 181 | return $taxRate ? $taxRate->percentage : 0; 182 | }); 183 | } 184 | 185 | /** 186 | * Determine if the invoice line item has tax rates. 187 | * 188 | * @return bool 189 | */ 190 | public function hasTaxRates(): bool 191 | { 192 | return isset($this->item->taxes) && ! empty($this->item->taxes); 193 | } 194 | 195 | /** 196 | * Get all taxes applied to this line item. 197 | * 198 | * @return \Illuminate\Support\Collection 199 | */ 200 | public function taxes(): Collection 201 | { 202 | if (! isset($this->item->taxes)) { 203 | return collect(); 204 | } 205 | 206 | return collect($this->item->taxes); 207 | } 208 | 209 | /** 210 | * Get tax rate details from the taxes array. 211 | * 212 | * @return \Illuminate\Support\Collection 213 | */ 214 | public function taxRateDetails(): Collection 215 | { 216 | return $this->taxes() 217 | ->filter(function (object $tax) { 218 | return $tax->type === 'tax_rate_details' && isset($tax->tax_rate_details); 219 | }) 220 | ->map(function (object $tax) { 221 | return $this->getTaxRate($tax->tax_rate_details); 222 | }) 223 | ->filter(); 224 | } 225 | 226 | /** 227 | * Get the tax rate from tax rate details, fetching from Stripe if needed. 228 | * 229 | * @param object $taxRateDetails 230 | * @return \Stripe\TaxRate|null 231 | */ 232 | protected function getTaxRate($taxRateDetails) 233 | { 234 | // If tax_rate is already expanded as an object, return it... 235 | if (isset($taxRateDetails->tax_rate->id) && is_object($taxRateDetails->tax_rate)) { 236 | return $taxRateDetails->tax_rate; 237 | } 238 | 239 | // If tax_rate is just an ID string, fetch it from Stripe... 240 | if (isset($taxRateDetails->tax_rate) && is_string($taxRateDetails->tax_rate)) { 241 | try { 242 | return $this->invoice->owner()->stripe()->taxRates->retrieve($taxRateDetails->tax_rate); 243 | } catch (Exception $e) { 244 | return null; 245 | } 246 | } 247 | 248 | return null; 249 | } 250 | 251 | /** 252 | * Get the total tax amount for this line item. 253 | * 254 | * @return int 255 | */ 256 | public function totalTaxAmount(): int 257 | { 258 | return $this->taxes()->sum('amount'); 259 | } 260 | 261 | /** 262 | * Get the tax behavior from the pricing structure. 263 | * 264 | * @return string|null 265 | */ 266 | public function taxBehavior(): ?string 267 | { 268 | // Get the price object and return its tax_behavior... 269 | $price = $this->price(); 270 | 271 | return $price ? ($price->tax_behavior ?? null) : null; 272 | } 273 | 274 | /** 275 | * Get a human readable date for the start date. 276 | * 277 | * @return string|null 278 | */ 279 | public function startDate(): ?string 280 | { 281 | if ($this->hasPeriod()) { 282 | return $this->startDateAsCarbon()->toFormattedDateString(); 283 | } 284 | 285 | return null; 286 | } 287 | 288 | /** 289 | * Get a human readable date for the end date. 290 | * 291 | * @return string|null 292 | */ 293 | public function endDate(): ?string 294 | { 295 | if ($this->hasPeriod()) { 296 | return $this->endDateAsCarbon()->toFormattedDateString(); 297 | } 298 | 299 | return null; 300 | } 301 | 302 | /** 303 | * Get a Carbon instance for the start date. 304 | * 305 | * @return \Carbon\CarbonInterface|null 306 | */ 307 | public function startDateAsCarbon(): ?CarbonInterface 308 | { 309 | if ($this->hasPeriod()) { 310 | return Carbon::createFromTimestampUTC($this->item->period->start); 311 | } 312 | 313 | return null; 314 | } 315 | 316 | /** 317 | * Get a Carbon instance for the end date. 318 | * 319 | * @return \Carbon\CarbonInterface|null 320 | */ 321 | public function endDateAsCarbon(): ?CarbonInterface 322 | { 323 | if ($this->hasPeriod()) { 324 | return Carbon::createFromTimestampUTC($this->item->period->end); 325 | } 326 | 327 | return null; 328 | } 329 | 330 | /** 331 | * Determine if the invoice line item has a defined period. 332 | * 333 | * @return bool 334 | */ 335 | public function hasPeriod(): bool 336 | { 337 | return ! is_null($this->item->period); 338 | } 339 | 340 | /** 341 | * Determine if the invoice line item has a period with the same start and end date. 342 | * 343 | * @return bool 344 | */ 345 | public function periodStartAndEndAreEqual(): bool 346 | { 347 | return $this->hasPeriod() ? $this->item->period->start === $this->item->period->end : false; 348 | } 349 | 350 | /** 351 | * Determine if the invoice line item is for a subscription. 352 | * 353 | * @return bool 354 | */ 355 | public function isSubscription(): bool 356 | { 357 | return isset($this->item->parent) && 358 | ($this->item->parent->type === 'subscription_details' || 359 | $this->item->parent->type === 'subscription_item_details'); 360 | } 361 | 362 | /** 363 | * Determine if the invoice line item is for an invoice item. 364 | * 365 | * @return bool 366 | */ 367 | public function isInvoiceItem(): bool 368 | { 369 | return isset($this->item->parent) && 370 | $this->item->parent->type === 'invoice_item_details'; 371 | } 372 | 373 | /** 374 | * Get the subscription ID associated with this line item. 375 | * 376 | * @return string|null 377 | */ 378 | public function subscriptionId(): ?string 379 | { 380 | if (! isset($this->item->parent)) { 381 | return null; 382 | } 383 | 384 | if ($this->item->parent->type === 'subscription_details') { 385 | return $this->item->parent->subscription_details->subscription ?? null; 386 | } 387 | 388 | if ($this->item->parent->type === 'subscription_item_details') { 389 | return $this->item->parent->subscription_item_details->subscription ?? null; 390 | } 391 | 392 | return null; 393 | } 394 | 395 | /** 396 | * Get the subscription item ID associated with this line item. 397 | * 398 | * @return string|null 399 | */ 400 | public function subscriptionItemId(): ?string 401 | { 402 | if (! isset($this->item->parent)) { 403 | return null; 404 | } 405 | 406 | if ($this->item->parent->type === 'subscription_item_details') { 407 | return $this->item->parent->subscription_item_details->subscription_item ?? null; 408 | } 409 | 410 | return null; 411 | } 412 | 413 | /** 414 | * Get the invoice item ID associated with this line item. 415 | * 416 | * @return string|null 417 | */ 418 | public function invoiceItemId(): ?string 419 | { 420 | if (! isset($this->item->parent)) { 421 | return null; 422 | } 423 | 424 | if ($this->item->parent->type === 'invoice_item_details') { 425 | return $this->item->parent->invoice_item_details->invoice_item ?? null; 426 | } 427 | 428 | return null; 429 | } 430 | 431 | /** 432 | * Determine if this line item is a proration. 433 | * 434 | * @return bool 435 | */ 436 | public function isProration(): bool 437 | { 438 | if (! isset($this->item->parent)) { 439 | return false; 440 | } 441 | 442 | if ($this->item->parent->type === 'subscription_item_details') { 443 | return $this->item->parent->subscription_item_details->proration ?? false; 444 | } 445 | 446 | if ($this->item->parent->type === 'invoice_item_details') { 447 | return $this->item->parent->invoice_item_details->proration ?? false; 448 | } 449 | 450 | return false; 451 | } 452 | 453 | /** 454 | * Get proration details for this line item. 455 | * 456 | * @return object|null 457 | */ 458 | public function prorationDetails(): ?object 459 | { 460 | if (! isset($this->item->parent)) { 461 | return null; 462 | } 463 | 464 | if ($this->item->parent->type === 'subscription_item_details') { 465 | return $this->item->parent->subscription_item_details->proration_details ?? null; 466 | } 467 | 468 | if ($this->item->parent->type === 'invoice_item_details') { 469 | return $this->item->parent->invoice_item_details->proration_details ?? null; 470 | } 471 | 472 | return null; 473 | } 474 | 475 | /** 476 | * Get the price ID from the pricing structure. 477 | * 478 | * @return string|null 479 | */ 480 | public function priceId(): ?string 481 | { 482 | // Handle the new pricing structure (Basil release)... 483 | if (isset($this->item->pricing) && $this->item->pricing->type === 'price_details') { 484 | return $this->item->pricing->price_details->price ?? null; 485 | } 486 | 487 | return null; 488 | } 489 | 490 | /** 491 | * Get the full price object from Stripe. 492 | * 493 | * @return object|null 494 | */ 495 | public function price(): ?object 496 | { 497 | if (isset($this->item->price) && is_object($this->item->price) && isset($this->item->price->id)) { 498 | return $this->item->price; 499 | } 500 | 501 | $priceId = $this->priceId(); 502 | 503 | if ($priceId && $this->invoice->owner()) { 504 | try { 505 | return $this->invoice->owner()->stripe()->prices->retrieve($priceId); 506 | } catch (\Exception $e) { 507 | return null; 508 | } 509 | } 510 | 511 | return null; 512 | } 513 | 514 | /** 515 | * Get the parent information for this line item. 516 | * 517 | * @return object|null 518 | */ 519 | public function parent(): ?object 520 | { 521 | return $this->item->parent ?? null; 522 | } 523 | 524 | /** 525 | * Format the given amount into a displayable currency. 526 | * 527 | * @param int $amount 528 | * @return string 529 | */ 530 | protected function formatAmount(int $amount): string 531 | { 532 | return Cashier::formatAmount($amount, $this->item->currency); 533 | } 534 | 535 | /** 536 | * Get the Stripe model instance. 537 | * 538 | * @return \Laravel\Cashier\Invoice 539 | */ 540 | public function invoice(): Invoice 541 | { 542 | return $this->invoice; 543 | } 544 | 545 | /** 546 | * Get the underlying Stripe invoice line item. 547 | * 548 | * @return \Stripe\InvoiceLineItem 549 | */ 550 | public function asStripeInvoiceLineItem() 551 | { 552 | return $this->item; 553 | } 554 | 555 | /** 556 | * Get the instance as an array. 557 | * 558 | * @return array 559 | */ 560 | public function toArray() 561 | { 562 | return $this->asStripeInvoiceLineItem()->toArray(); 563 | } 564 | 565 | /** 566 | * Convert the object to its JSON representation. 567 | * 568 | * @param int $options 569 | * @return string 570 | */ 571 | public function toJson($options = 0) 572 | { 573 | return json_encode($this->jsonSerialize(), $options); 574 | } 575 | 576 | /** 577 | * Convert the object into something JSON serializable. 578 | * 579 | * @return array 580 | */ 581 | #[\ReturnTypeWillChange] 582 | public function jsonSerialize() 583 | { 584 | return $this->toArray(); 585 | } 586 | 587 | /** 588 | * Dynamically access the Stripe invoice line item instance. 589 | * 590 | * @param string $key 591 | * @return mixed 592 | */ 593 | public function __get(string $key) 594 | { 595 | return $this->item->{$key}; 596 | } 597 | } 598 | --------------------------------------------------------------------------------