├── .gitignore ├── routes └── webhook.php ├── src ├── Concerns │ ├── ManagesPerson.php │ ├── ManagesConnectCustomer.php │ ├── ManagesBalance.php │ ├── ManagesApplePayDomain.php │ ├── ManagesPayout.php │ ├── ManageConnectedPaymentMethods.php │ ├── ManagesTransfer.php │ ├── ManagesTerminals.php │ ├── CanCharge.php │ ├── ManagesAccountLink.php │ ├── ManagesConnectProducts.php │ ├── ManagesPaymentLinks.php │ ├── ManageCustomer.php │ ├── ManagesConnectSubscriptions.php │ └── ManagesAccount.php ├── Models │ ├── TestModel.php │ ├── ConnectCustomer.php │ ├── ConnectSubscriptionItem.php │ ├── ConnectMapping.php │ └── ConnectSubscription.php ├── Exceptions │ ├── AccountNotFoundException.php │ └── AccountAlreadyExistsException.php ├── Events │ ├── ConnectWebhookHandled.php │ └── ConnectWebhookReceived.php ├── Contracts │ └── StripeAccount.php ├── Http │ ├── Middleware │ │ └── VerifyConnectWebhook.php │ └── Controllers │ │ └── WebhookController.php ├── Console │ └── ConnectWebhook.php ├── StripeEntity.php ├── CashierConnectServiceProvider.php ├── Billable.php └── ConnectCustomer.php ├── config └── cashierconnect.php ├── database └── migrations │ ├── 2020_12_04_000020_add__type_account_column.php │ ├── 2020_12_01_000001_create_account_columns.php │ ├── 2025_11_18_000001_add_meter_columns_to_connected_subscription_items.php │ ├── 2020_12_02_000002_create_connected_account_customer_columns.php │ ├── 2020_12_01_000020_add_account_columns.php │ ├── 2020_12_03_000011_create_connected_subscription_items_table.php │ └── 2020_12_03_000001_create_connected_subscription_table.php ├── LICENSE ├── composer.json ├── CHANGELOG.md ├── UPGRADE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor 3 | composer.lock 4 | /phpunit.xml 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /routes/webhook.php: -------------------------------------------------------------------------------- 1 | name('stripeConnect.webhook'); 7 | -------------------------------------------------------------------------------- /src/Concerns/ManagesPerson.php: -------------------------------------------------------------------------------- 1 | 'string', 20 | ]; 21 | 22 | public function subscription() 23 | { 24 | return $this->belongsTo(ConnectSubscription::class, 'connected_subscription_id', 'id'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/ConnectWebhookHandled.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/Events/ConnectWebhookReceived.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/Models/ConnectMapping.php: -------------------------------------------------------------------------------- 1 | 'object', 17 | "requirements" => 'object' 18 | ]; 19 | 20 | public $timestamps = false; 21 | 22 | protected $table = 'stripe_connect_mappings'; 23 | 24 | public function subscriptions(){ 25 | return $this->hasMany(ConnectSubscription::class, 'stripe_account_id', 'stripe_account_id'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /config/cashierconnect.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'secret' => env('CONNECT_WEBHOOK_SECRET'), 7 | 'tolerance' => env('CONNECT_WEBHOOK_TOLERANCE', 300) 8 | ], 9 | 10 | 'events' => [ 11 | // SUBSCRIPTION ONES 12 | 'customer.subscription.created', 13 | 'customer.subscription.updated', 14 | 'customer.subscription.deleted', 15 | 'customer.updated', 16 | 'customer.deleted', 17 | 'invoice.payment_action_required', 18 | 'invoice.payment_succeeded', 19 | // DIRECT CHARGE PAYMENTS 20 | 'charge.succeeded' 21 | ], 22 | 23 | /** Used when the model doesn't have a currency assigned to it or the currency isn't provided by the function */ 24 | 25 | 'currency' => env('CASHIER_CONNECT_CURRENCY', 'usd') 26 | 27 | 28 | ]; -------------------------------------------------------------------------------- /src/Concerns/ManagesConnectCustomer.php: -------------------------------------------------------------------------------- 1 | hasMany(ConnectSubscriptionItem::class, 'connected_subscription_id', 'id'); 20 | } 21 | 22 | /** 23 | * Gets the stripe subscription for the model 24 | * @return Subscription 25 | * @throws ApiErrorException 26 | */ 27 | public function asStripeSubscription(){ 28 | return Subscription::retrieve($this->stripe_id, $this->stripeAccountOptions([], $this->stripe_account_id)); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2020_12_04_000020_add__type_account_column.php: -------------------------------------------------------------------------------- 1 | string('type')->default('standard'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::table('stripe_connect_mappings', function (Blueprint $table) { 33 | $table->dropColumn('type'); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/Concerns/ManagesBalance.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 27 | 28 | // Create the payload for retrieving balance. 29 | $options = array_merge([ 30 | 'stripe_account' => $this->stripeAccountId(), 31 | ], $this->stripeAccountOptions()); 32 | 33 | return Balance::retrieve($options); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2020_12_01_000001_create_account_columns.php: -------------------------------------------------------------------------------- 1 | string('model'); 22 | $table->unsignedBigInteger('model_id')->nullable()->index();; 23 | $table->uuid('model_uuid')->nullable()->index(); 24 | $table->string('stripe_account_id')->index(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('stripe_connect_mappings'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2025_11_18_000001_add_meter_columns_to_connected_subscription_items.php: -------------------------------------------------------------------------------- 1 | string('meter_event_name')->nullable()->after('quantity'); 22 | $table->string('meter_id')->nullable()->after('meter_event_name'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::table('connected_subscription_items', function (Blueprint $table) { 34 | $table->dropColumn(['meter_event_name', 'meter_id']); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robert Lane 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Contracts/StripeAccount.php: -------------------------------------------------------------------------------- 1 | string('model'); 22 | $table->unsignedBigInteger('model_id')->nullable()->index();; 23 | $table->uuid('model_uuid')->nullable()->index(); 24 | $table->string('stripe_customer_id')->index(); 25 | $table->string('stripe_account_id')->index(); // FOR RELATING A CONNECTED CUSTOMER MODEL TO A CONNECTED ACCOUNT 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('stripe_connected_customer_mappings'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifyConnectWebhook.php: -------------------------------------------------------------------------------- 1 | getContent(), 26 | $request->header('Stripe-Signature'), 27 | config('cashierconnect.webhook.secret'), 28 | config('cashierconnect.webhook.tolerance') 29 | ); 30 | } catch (SignatureVerificationException $exception) { 31 | throw new AccessDeniedHttpException($exception->getMessage(), $exception); 32 | } 33 | 34 | return $next($request); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Concerns/ManagesApplePayDomain.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 30 | return ApplePayDomain::create(['domain_name' => $domain], $this->stripeAccountOptions([], true)); 31 | 32 | } 33 | 34 | /** 35 | * @return Collection 36 | * @throws AccountNotFoundException 37 | * @throws ApiErrorException 38 | */ 39 | public function getApplePayDomains(){ 40 | $this->assertAccountExists(); 41 | return ApplePayDomain::all([], $this->stripeAccountOptions([], true)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_12_01_000020_add_account_columns.php: -------------------------------------------------------------------------------- 1 | json('future_requirements')->nullable(); 22 | $table->boolean('charges_enabled')->default(false); 23 | $table->boolean('first_onboarding_done')->default(false); 24 | $table->json('requirements')->nullable(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::table('stripe_connect_mappings', function (Blueprint $table) { 36 | $table->dropColumn('future_requirements'); 37 | $table->dropColumn('charges_enabled'); 38 | $table->dropColumn('first_onboarding_done'); 39 | $table->dropColumn('requirements'); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/migrations/2020_12_03_000011_create_connected_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 22 | $table->unsignedBigInteger('connected_subscription_id'); // THE INCREMENTING ID OF THE CONNECTED SUBSCRIPTION 23 | $table->string('stripe_id'); // THE SI ID IN STRIPE 24 | $table->string('connected_product'); // THE ID OF THE PRODUCT WITHIN THE CONNECTED ACCOUNT 25 | $table->string('connected_price'); // THE ID OF THE PRICE WITHIN THE CONNECTED ACCOUNT 26 | $table->unsignedBigInteger('quantity'); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('connected_subscription_items'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/Concerns/ManagesPayout.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 33 | 34 | // Create the payload for payout. 35 | $options = array_merge($options, [ 36 | 'amount' => $amount, 37 | 'currency' => Str::lower($currency), 38 | 'arrival_date' => $arrival->timestamp, 39 | ]); 40 | 41 | return Payout::create($options, $this->stripeAccountOptions([], true)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_12_03_000001_create_connected_subscription_table.php: -------------------------------------------------------------------------------- 1 | id(); 22 | $table->string('name'); 23 | $table->string('stripe_id'); 24 | $table->string('stripe_status'); 25 | $table->string('connected_price_id'); 26 | $table->unsignedBigInteger('quantity')->nullable(); 27 | $table->timestamp('trial_ends_at')->nullable(); 28 | $table->timestamp('ends_at')->nullable(); 29 | $table->timestamps(); 30 | $table->string('stripe_customer_id')->index(); 31 | $table->string('stripe_account_id')->index()->nullable(); // FOR RELATING A CONNECTED CUSTOMER MODEL TO A CONNECTED ACCOUNT 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('connected_subscriptions'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lanos/laravel-cashier-stripe-connect", 3 | "description": "Adds Stripe Connect functionality to Laravel's main billing package, Cashier.", 4 | "keywords": ["laravel", "stripe", "stripe-connect", "billing"], 5 | "type": "library", 6 | "require": { 7 | "php" : "^7.4|^8.1|^8.2|^8.3|^8.4", 8 | "laravel/cashier": "^12.6|^13.4|^v14.6.0|^v15.3.0|^16.0", 9 | "illuminate/console": "^9.0|^10.0|^11.0|^12.0", 10 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", 11 | "illuminate/database": "^9.0|^10.0|^11.0|^12.0", 12 | "illuminate/http": "^9.0|^10.0|^11.0|^12.0", 13 | "illuminate/log": "^9.0|^10.0|^11.0|^12.0", 14 | "illuminate/notifications": "^9.0|^10.0|^11.0|^12.0", 15 | "illuminate/routing": "^9.0|^10.0|^11.0|^12.0", 16 | "illuminate/support": "^9.0|^10.0|^11.0|^12.0", 17 | "illuminate/view": "^9.0|^10.0|^11.0|^12.0" 18 | }, 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "Robert Lane", 23 | "email": "rob@updev.agency" 24 | } 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Lanos\\CashierConnect\\": "src/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Lanos\\CashierConnect\\CashierConnectServiceProvider" 35 | ] 36 | } 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true 40 | } 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.3.0] - 2025-11-18 6 | 7 | ### Added 8 | - Support for Laravel Cashier 16.x 9 | - New columns in `connected_subscription_items` table: 10 | - `meter_event_name` (nullable) - For metered billing support 11 | - `meter_id` (nullable) - For metered billing support 12 | - Compatibility with Stripe API version `2025-07-30.basil` 13 | - `UPGRADE.md` file with detailed upgrade instructions 14 | 15 | ### Changed 16 | - Updated `laravel/cashier` version constraint to include `^16.0` 17 | - Added `meter_id` cast as `string` in `ConnectSubscriptionItem` model 18 | 19 | ## [1.2.3] 20 | 21 | ### Added 22 | - Payment Links functionality for connected accounts 23 | - Creation of both Direct and Destination payment links, including "on behalf of" support 24 | - Support for percentage and fixed application fees on payment links 25 | - Retrieval of all direct payment links for a connected account 26 | 27 | ## [1.2.2] 28 | 29 | ### Added 30 | - Functionality for physical terminals and Apple/Android tap to pay 31 | - Adding terminal locations 32 | - Adding a reader and associating it with a terminal 33 | - Handling connection token requests 34 | 35 | ## [1.1.0] 36 | 37 | ### Changed 38 | - Compatibility with Cashier 15 39 | - Migrations are no longer auto-published, must now be published using the `vendor:publish` command 40 | - Updated to Stripe API version 2023-10-16 41 | 42 | ### Removed 43 | - Support for `ignoreMigrations()` - can be safely removed from code 44 | 45 | ## Earlier Versions 46 | 47 | See Git history for changes in versions prior to 1.1.0. 48 | 49 | -------------------------------------------------------------------------------- /src/Concerns/ManageConnectedPaymentMethods.php: -------------------------------------------------------------------------------- 1 | assetCustomerExists(); 25 | return Customer::allPaymentMethods($this->stripeCustomerId(), $this->stripeAccountOptions($this->stripeAccountId())); 26 | } 27 | 28 | /** 29 | * Detaches the payment method from the customer 30 | * @param $id 31 | * @return PaymentMethod 32 | * @throws ApiErrorException 33 | * @throws Exception 34 | */ 35 | public function removePaymentMethod($id){ 36 | $this->assetCustomerExists(); 37 | 38 | $method = PaymentMethod::retrieve($id, $this->stripeAccountOptions($this->stripeAccountId())); 39 | 40 | if(!$method->customer === $this->stripeAccountId()){ 41 | throw new Exception('This payment method doesn\'t belong to this customer or is invalid'); 42 | } 43 | 44 | return PaymentMethod::detach($id, $this->stripeAccountOptions($this->stripeAccountId())); 45 | 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/Console/ConnectWebhook.php: -------------------------------------------------------------------------------- 1 | webhookEndpoints; 36 | 37 | $endpoint = $webhookEndpoints->create([ 38 | 'enabled_events' => config('cashierconnect.events'), 39 | 'url' => $this->option('url') ?? route('stripeConnect.webhook'), 40 | 'api_version' => $this->option('api-version') ?? Cashier::STRIPE_VERSION, 41 | 'connect' => true 42 | ]); 43 | 44 | $this->components->info('The Stripe webhook was created successfully. Retrieve the webhook secret in your Stripe dashboard and define it as an environment variable.'); 45 | 46 | if ($this->option('disabled')) { 47 | $webhookEndpoints->update($endpoint->id, ['disabled' => true]); 48 | 49 | $this->components->info('The Stripe webhook was disabled as requested. You may enable the webhook via the Stripe dashboard when needed.'); 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/StripeEntity.php: -------------------------------------------------------------------------------- 1 | $stripeOptions->getApiKey() 53 | ]); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading to 1.3.0 (Cashier 16.x) 4 | 5 | ### Major Changes 6 | 7 | Laravel Cashier Stripe Connect 1.3.0 introduces compatibility with Laravel Cashier 16.x, which brings support for Stripe's new metered billing APIs (Stripe Billing Meters). 8 | 9 | ### Upgrade Steps 10 | 11 | #### 1. Update Your Dependencies 12 | 13 | Update your `composer.json` file: 14 | 15 | ```bash 16 | composer update 17 | ``` 18 | 19 | #### 2. Publish and Run Migrations 20 | 21 | Two new columns have been added to the `connected_subscription_items` table: 22 | - `meter_event_name` (nullable) - The meter event name for metered billing 23 | - `meter_id` (nullable) - The Stripe meter identifier 24 | 25 | Publish the new migrations: 26 | 27 | ```bash 28 | php artisan vendor:publish --tag="cashier-connect-migrations" --force 29 | ``` 30 | 31 | Run the migrations: 32 | 33 | ```bash 34 | php artisan migrate 35 | ``` 36 | 37 | #### 3. Update Your Stripe API Version 38 | 39 | After deploying this update to production, log in to your Stripe dashboard and update your API version to `2025-07-30.basil` to take full advantage of the new features. 40 | 41 | **Important:** Test in a staging environment first. Older Stripe accounts may encounter compatibility issues with the new Basil APIs or require manual API key upgrades. 42 | 43 | ### Database Changes 44 | 45 | The `connected_subscription_items` table now has two new columns: 46 | 47 | | Column | Type | Description | 48 | |---------|------|-------------| 49 | | `meter_event_name` | string (nullable) | Event name for metered billing | 50 | | `meter_id` | string (nullable) | Stripe meter ID | 51 | 52 | ### Compatibility 53 | 54 | - Laravel Cashier: ^16.0 55 | - Stripe API: 2025-07-30.basil 56 | - PHP: ^7.4\|^8.1\|^8.2\|^8.3\|^8.4 57 | - Laravel: ^9.0\|^10.0\|^11.0\|^12.0 58 | 59 | ### Notes 60 | 61 | - Changes primarily concern metered billing 62 | - If you don't use metered billing, the new columns will remain NULL 63 | - Backward compatibility with previous Cashier versions (12.x, 13.x, 14.x, 15.x) is maintained 64 | - No existing code modifications are required if you don't use metered billing 65 | 66 | -------------------------------------------------------------------------------- /src/Concerns/ManagesTransfer.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 32 | 33 | $currency = $this->establishTransferCurrency($currencyToUse); 34 | 35 | // Create payload for the transfer. 36 | $options = array_merge([ 37 | 'destination' => $this->stripeAccountId(), 38 | 'amount' => $amount, 39 | 'currency' => Str::lower($currency), 40 | ], $options); 41 | 42 | return Transfer::create($options, $this->stripeAccountOptions()); 43 | } 44 | 45 | /** 46 | * Reverses a transfer back to the Connect Platform. This means the Stripe account will 47 | * 48 | * @param Transfer $transfer The transfer to reverse. 49 | * @param bool $refundFee Whether to refund the application fee too. 50 | * @param int|null $amount The amount to reverse. 51 | * @param array $options Any additional options. 52 | * @return TransferReversal 53 | * @throws AccountNotFoundException|ApiErrorException 54 | */ 55 | public function reverseTransferFromStripeAccount(Transfer $transfer, $refundFee = false, ?int $amount = null, array $options = []): TransferReversal 56 | { 57 | $this->assertAccountExists(); 58 | 59 | // Create payload for the transfer reversal. 60 | $options = array_merge([ 61 | 'amount' => $amount, 62 | 'refund_application_fee' => $refundFee, 63 | ], $options); 64 | 65 | return Transfer::createReversal($transfer->id, $options, $this->stripeAccountOptions()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Http/Controllers/WebhookController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifyConnectWebhook::class); 26 | } 27 | } 28 | 29 | /** 30 | * Handle a Stripe webhook call. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return \Symfony\Component\HttpFoundation\Response 34 | */ 35 | public function handleWebhook(Request $request) 36 | { 37 | $payload = json_decode($request->getContent(), true); 38 | $method = 'handle'.Str::studly(str_replace('.', '_', $payload['type'])); 39 | 40 | ConnectWebhookReceived::dispatch($payload); 41 | 42 | if (method_exists($this, $method)) { 43 | $this->setMaxNetworkRetries(); 44 | 45 | $response = $this->{$method}($payload); 46 | 47 | ConnectWebhookHandled::dispatch($payload); 48 | 49 | return $response; 50 | } 51 | 52 | return $this->missingMethod($payload); 53 | } 54 | 55 | /** 56 | * Handle successful calls on the controller. 57 | * 58 | * @param array $parameters 59 | * @return \Symfony\Component\HttpFoundation\Response 60 | */ 61 | protected function successMethod($parameters = []) 62 | { 63 | return new Response('Webhook Handled', 200); 64 | } 65 | 66 | /** 67 | * Handle calls to missing methods on the controller. 68 | * 69 | * @param array $parameters 70 | * @return \Symfony\Component\HttpFoundation\Response 71 | */ 72 | protected function missingMethod($parameters = []) 73 | { 74 | return new Response; 75 | } 76 | 77 | /** 78 | * Set the number of automatic retries due to an object lock timeout from Stripe. 79 | * 80 | * @param int $retries 81 | * @return void 82 | */ 83 | protected function setMaxNetworkRetries($retries = 3) 84 | { 85 | Stripe::setMaxNetworkRetries($retries); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/CashierConnectServiceProvider.php: -------------------------------------------------------------------------------- 1 | initializePublishing(); 25 | $this->initializeCommands(); 26 | $this->setupRoutes(); 27 | $this->setupConfig(); 28 | } 29 | 30 | public function register() 31 | { 32 | $this->mergeConfigFrom( 33 | __DIR__.'/../config/cashierconnect.php', 'cashierconnect' 34 | ); 35 | } 36 | 37 | /** 38 | * Register the package's publishable resources. 39 | * 40 | * @return void 41 | */ 42 | protected function initializePublishing() 43 | { 44 | if ($this->app->runningInConsole()) { 45 | 46 | $publishesMigrationsMethod = method_exists($this, 'publishesMigrations') 47 | ? 'publishesMigrations' 48 | : 'publishes'; 49 | 50 | $this->{$publishesMigrationsMethod}([ 51 | __DIR__.'/../database/migrations' => $this->app->databasePath('migrations'), 52 | ], 'cashier-connect-migrations'); 53 | $this->{$publishesMigrationsMethod}([ 54 | __DIR__.'/../database/migrations' => $this->app->databasePath('migrations/tenant'), 55 | ], 'cashier-connect-tenancy-migrations'); 56 | } 57 | } 58 | 59 | /** 60 | * Register the package's console commands. 61 | * 62 | * @return void 63 | */ 64 | protected function initializeCommands() 65 | { 66 | if ($this->app->runningInConsole()) { 67 | $this->commands([ 68 | ConnectWebhook::class 69 | ]); 70 | } 71 | } 72 | 73 | /** 74 | * Register the package's console commands. 75 | * 76 | * @return void 77 | */ 78 | protected function setupRoutes() 79 | { 80 | $this->loadRoutesFrom(__DIR__.'/../routes/webhook.php'); 81 | 82 | } 83 | 84 | /** 85 | * Register the package's config. 86 | * 87 | * @return void 88 | */ 89 | protected function setupConfig() 90 | { 91 | $this->publishes([ 92 | __DIR__.'/../config/cashierconnect.php' => config_path('cashierconnect.php'), 93 | ]); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Concerns/ManagesTerminals.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 29 | return Location::create($data, $this->stripeAccountOptions([], $direct)); 30 | } 31 | 32 | /** 33 | * @param array $params 34 | * @param bool $direct 35 | * @return Collection 36 | * @throws AccountNotFoundException 37 | * @throws ApiErrorException 38 | */ 39 | public function getTerminalLocations(array $params, bool $direct = false): Collection{ 40 | $this->assertAccountExists(); 41 | return Location::all($params, $this->stripeAccountOptions([], $direct)); 42 | } 43 | 44 | 45 | /** 46 | * @param array $data 47 | * @param bool $direct 48 | * @return Reader 49 | * @throws AccountNotFoundException 50 | * @throws ApiErrorException 51 | */ 52 | public function registerTerminalReader(array $data, bool $direct = false): Reader{ 53 | $this->assertAccountExists(); 54 | return Reader::create($data, $this->stripeAccountOptions([], $direct)); 55 | } 56 | 57 | /** 58 | * @param array $params 59 | * @param bool $direct 60 | * @return Collection 61 | * @throws AccountNotFoundException 62 | * @throws ApiErrorException 63 | */ 64 | public function getTerminalReaders(array $params = [], bool $direct = false): Collection{ 65 | $this->assertAccountExists(); 66 | return Reader::all($params, $this->stripeAccountOptions([], $direct)); 67 | } 68 | 69 | /** 70 | * @param string $location 71 | * @param bool $direct 72 | * @return ConnectionToken 73 | * @throws AccountNotFoundException 74 | * @throws ApiErrorException 75 | */ 76 | public function createConnectionToken(array $params = [], bool $direct = false): ConnectionToken{ 77 | $this->assertAccountExists(); 78 | return ConnectionToken::create($params, $this->stripeAccountOptions([], $direct)); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Billable.php: -------------------------------------------------------------------------------- 1 | hasStripeAccount()) { 54 | $options['stripe_account'] = $this->stripeAccountId(); 55 | } 56 | 57 | // Workaround for Cashier 12.x 58 | if (version_compare(Cashier::VERSION, '12.15.0', '<=')) { 59 | return array_merge(Cashier::stripeOptions($options)); 60 | } 61 | 62 | $stripeOptions = Cashier::stripe($options); 63 | 64 | return array_merge($options, [ 65 | 'api_key' => $stripeOptions->getApiKey() 66 | ]); 67 | } 68 | 69 | /** 70 | * @param $providedCurrency 71 | * @return mixed|string 72 | */ 73 | public function establishTransferCurrency(?string $providedCurrency = null){ 74 | 75 | if($providedCurrency){ 76 | return $providedCurrency; 77 | } 78 | 79 | if($this->defaultCurrency){ 80 | return $this->defaultCurrency; 81 | } 82 | 83 | return config('cashierconnect.currency'); 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/ConnectCustomer.php: -------------------------------------------------------------------------------- 1 | hasStripeAccount()) { 55 | $options['stripe_account'] = $connectedAccount->stripeAccountId(); 56 | }else{ 57 | throw new AccountNotFoundException('The '.class_basename($connectedAccount).' model does not have a Stripe Account.'); 58 | } 59 | } 60 | 61 | // Workaround for Cashier 12.x 62 | if (version_compare(Cashier::VERSION, '12.15.0', '<=')) { 63 | return array_merge(Cashier::stripeOptions($options)); 64 | } 65 | 66 | $stripeOptions = Cashier::stripe($options); 67 | 68 | return array_merge($options, [ 69 | 'api_key' => $stripeOptions->getApiKey() 70 | ]); 71 | } 72 | 73 | /** 74 | * Determine if the entity has a Stripe account ID and throw an exception if not. 75 | * 76 | * @return void 77 | * @throws AccountNotFoundException 78 | */ 79 | public function assetCustomerExists(): void 80 | { 81 | if (! $this->hasCustomerRecord()) { 82 | throw new AccountNotFoundException('Stripe customer does not exist.'); 83 | } 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Concerns/CanCharge.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 35 | 36 | // Create payload for the transfer. 37 | $options = array_merge([ 38 | 'amount' => $amount, 39 | 'currency' => Str::lower($this->establishTransferCurrency($currencyToUse)), 40 | ], $options); 41 | 42 | // APPLY PLATFORM FEE COMMISSION - SET THIS AGAINST THE MODEL 43 | if (isset($this->commission_type) && isset($this->commission_rate)) { 44 | if ($this->commission_type === 'percentage') { 45 | $options['application_fee_amount'] = round($this->calculatePercentageFee($amount)); 46 | } else { 47 | $options['application_fee_amount'] = round($this->commission_rate); 48 | } 49 | } 50 | 51 | 52 | return PaymentIntent::create($options, $this->stripeAccountOptions([],true)); 53 | 54 | } 55 | 56 | /** 57 | * @param int $amount 58 | * @param string|null $currencyToUse 59 | * @param array $options 60 | * @param bool $onBehalfOf 61 | * @return PaymentIntent 62 | * @throws AccountNotFoundException 63 | * @throws ApiErrorException 64 | */ 65 | public function createDestinationCharge(int $amount, ?string $currencyToUse = null, array $options = [], bool $onBehalfOf = false): PaymentIntent 66 | { 67 | 68 | $this->assertAccountExists(); 69 | 70 | // Create payload for the transfer. 71 | $options = array_merge([ 72 | 'amount' => $amount, 73 | 'transfer_data' => [ 74 | 'destination' => $this->stripeAccountId() 75 | ], 76 | 'currency' => Str::lower($this->establishTransferCurrency($currencyToUse)), 77 | ], $options); 78 | 79 | if($onBehalfOf){ 80 | $options['on_behalf_of'] = $this->stripeAccountId(); 81 | } 82 | 83 | // APPLY PLATFORM FEE COMMISSION - SET THIS AGAINST THE MODEL 84 | if (isset($this->commission_type) && isset($this->commission_rate)) { 85 | if ($this->commission_type === 'percentage') { 86 | $options['application_fee_amount'] = ceil($this->calculatePercentageFee($amount)); 87 | } else { 88 | $options['application_fee_amount'] = ceil($this->commission_rate); 89 | } 90 | } 91 | 92 | return PaymentIntent::create($options, $this->stripeAccountOptions()); 93 | 94 | } 95 | 96 | 97 | /** 98 | * @param $amount 99 | * @return float|int 100 | * @throws \Exception 101 | */ 102 | private function calculatePercentageFee($amount){ 103 | if($this->commission_rate < 100){ 104 | return ($this->commission_rate / 100) * $amount; 105 | }else{ 106 | throw new \Exception('You cannot charge more than 100% fee.'); 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Concerns/ManagesAccountLink.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 31 | 32 | $options = array_merge([ 33 | 'type' => $type, 34 | 'account' => $this->stripeAccountId(), 35 | ], $options); 36 | 37 | return AccountLink::create($options, $this->stripeAccountOptions())->url; 38 | } 39 | 40 | /** 41 | * Generates a redirect response to the account link URL for Stripe. 42 | * 43 | * @param $type 44 | * @param $options 45 | * @return RedirectResponse 46 | * @throws AccountNotFoundException|ApiErrorException 47 | */ 48 | public function redirectToAccountLink(string $type, array $options = []): RedirectResponse 49 | { 50 | return new RedirectResponse($this->accountLinkUrl($type, $options)); 51 | } 52 | 53 | /** 54 | * Gets an URL for Stripe account onboarding. 55 | * 56 | * @param $return_url 57 | * @param $refresh_url 58 | * @param $options 59 | * @return string 60 | * @throws AccountNotFoundException|ApiErrorException 61 | */ 62 | public function accountOnboardingUrl(string $return_url, string $refresh_url, array $options = []): string 63 | { 64 | $options = array_merge([ 65 | 'return_url' => $return_url, 66 | 'refresh_url' => $refresh_url, 67 | ], $options); 68 | 69 | return $this->accountLinkUrl('account_onboarding', $options); 70 | } 71 | 72 | /** 73 | * Generates a redirect response to the account onboarding URL for Stripe. 74 | * 75 | * @param $return_url 76 | * @param $refresh_url 77 | * @param $options 78 | * @return RedirectResponse 79 | * @throws AccountNotFoundException|ApiErrorException 80 | */ 81 | public function redirectToAccountOnboarding(string $return_url, string $refresh_url, array $options= []) 82 | { 83 | return new RedirectResponse($this->accountOnboardingUrl($return_url, $refresh_url, $options)); 84 | } 85 | 86 | /** 87 | * Gets the Stripe account dashboard login URL. 88 | * 89 | * @param $options 90 | * @return string 91 | * @throws AccountNotFoundException|ApiErrorException 92 | */ 93 | public function accountDashboardUrl(array $options = []): ?string 94 | { 95 | $this->assertAccountExists(); 96 | 97 | // Can only create login link if details has been submitted. 98 | return $this->hasSubmittedAccountDetails() 99 | ? Account::createLoginLink($this->stripeAccountId(), $options, $this->stripeAccountOptions())->url 100 | : null; 101 | } 102 | 103 | /** 104 | * Generates a redirect response to the account dashboard login for Stripe. 105 | * 106 | * @return RedirectResponse 107 | * @throws AccountNotFoundException|ApiErrorException 108 | */ 109 | public function redirectToAccountDashboard(): RedirectResponse 110 | { 111 | return new RedirectResponse($this->accountDashboardUrl()); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/Concerns/ManagesConnectProducts.php: -------------------------------------------------------------------------------- 1 | assertAccountExists(); 32 | return Product::all(null, $this->stripeAccountOptions([], true)); 33 | } 34 | 35 | /** 36 | * This will retrieve a single product that belongs to the connected account 37 | * @param $id 38 | * @return Product 39 | * @throws ApiErrorException 40 | */ 41 | public function getSingleConnectedProduct($id): Product 42 | { 43 | $this->assertAccountExists(); 44 | return Product::retrieve($id, $this->stripeAccountOptions([], true)); 45 | } 46 | 47 | /** 48 | * Creates a stripe product against the connected account 49 | * @param $data 50 | * @return Product 51 | * @throws ApiErrorException 52 | */ 53 | public function createConnectedProduct($data): Product 54 | { 55 | $this->assertAccountExists(); 56 | return Product::create($data, $this->stripeAccountOptions([], true)); 57 | } 58 | 59 | /** 60 | * Edits a stripe product against the connected account 61 | * @param $id 62 | * @param $data 63 | * @return Product 64 | * @throws ApiErrorException 65 | */ 66 | public function editConnectedProduct($id, $data): Product 67 | { 68 | $this->assertAccountExists(); 69 | return Product::update($id, $data, $this->stripeAccountOptions([], true)); 70 | } 71 | 72 | /** 73 | * @param $id 74 | * @return Collection 75 | * @throws ApiErrorException 76 | */ 77 | public function getPricesForConnectedProduct($id): Collection 78 | { 79 | $this->assertAccountExists(); 80 | return Price::all([ 81 | "product" => $id 82 | ], $this->stripeAccountOptions([], true) ); 83 | } 84 | 85 | /** 86 | * Creates a price for a product on a connected account 87 | * @param $id 88 | * @param $data 89 | * @return Price 90 | * @throws ApiErrorException 91 | * @throws AccountNotFoundException 92 | */ 93 | public function createPriceForConnectedProduct($id, $data): Price 94 | { 95 | $this->assertAccountExists(); 96 | return Price::create($data + [ 97 | "product" => $id 98 | ], $this->stripeAccountOptions([], true) ); 99 | } 100 | 101 | /** 102 | * Gets single price against a product against a connected account 103 | * @param $id 104 | * @return Price 105 | * @throws ApiErrorException 106 | * @throws AccountNotFoundException 107 | */ 108 | public function getSingleConnectedPrice($id): Price 109 | { 110 | $this->assertAccountExists(); 111 | return Price::retrieve($id, $this->stripeAccountOptions([], true) ); 112 | } 113 | 114 | /** 115 | * Edits a stripe price against the connected account 116 | * @param $id 117 | * @param $data 118 | * @return Price 119 | * @throws ApiErrorException 120 | * @throws AccountNotFoundException 121 | */ 122 | public function editConnectedPrice($id, $data): Price 123 | { 124 | $this->assertAccountExists(); 125 | return Price::update($id, $data, $this->stripeAccountOptions([], true)); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Cashier For Connect 3 |

4 | 5 | ### Help me to keep helping you. 6 | Working on open source packages and helping other developers is my true passion, unfortunately I have to work in order to pay bills. The more people that help me out, the more time I can spend building cool packages and supporting developers. It doesn't have to be much, just a cup of coffee's worth. It's all appreciated! 7 | 8 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/E1E0ZF7W0) 9 | 10 | ### Documentation has been updated to cover the new features introduced in 1.2.2. 11 | 12 | ## V1.3.0 Update (Cashier 16) 13 | This update brings compatibility with Laravel Cashier 16.x which introduces support for Stripe's new metered billing API (Stripe Billing Meters). Changes include: 14 | 15 | - Support for Laravel Cashier ^16.0 16 | - Added new columns to the `connected_subscription_items` table: `meter_event_name` and `meter_id` 17 | - Compatibility with Stripe API version `2025-07-30.basil` 18 | 19 | To upgrade, run: 20 | ```bash 21 | composer update 22 | php artisan vendor:publish --tag="cashier-connect-migrations" 23 | php artisan migrate 24 | ``` 25 | 26 | **Important Note:** After deploying this update, remember to update your Stripe API version in your Stripe dashboard to `2025-07-30.basil` to take full advantage of the new features. 27 | 28 | ## V1.2.3 Update 29 | This update bring new functionality for users wishing to use payment links with their connected accounts: 30 | - Create both Direct and Destination payment links, including using "on behalf of". 31 | - Utilise both percentage and fixed application fees on payment links 32 | - Retrieve all direct payment links for a connected account 33 | - Note: It's difficult and impractical to retrieve all destination payment links as they are currently not filterable by connected account directly on the Stripe API, i've requested they add this in, if they do i'll update the plugin. Until then if you want to store and return them to your users, you need to store a copy locally on your application database. 34 | 35 | ## V1.2.0 Update 36 | This update bring new functionality for users wishing to use both physical terminals and the new Apple/Android tap to pay functionality. It will facilitate the use of: 37 | - Adding terminal locations 38 | - Adding a reader and associating it with a terminal 39 | - Handling connection token requests 40 | 41 | ## V1.1.0 Update (Cashier 15) 42 | The Cashier 15 update brought about a few changes to the package. These include: 43 | 44 | - Migrations no longer auto publish, you must now publish them using the command stated in the GitBook readme. 45 | - Any use of ignoreMigrations() in your code can be and should be safely removed 46 | - Stripe API Version is now 2023-10-16, changes have been made to accommodate this 47 | 48 | ## Intro 49 | 50 | This package is designed to seamlessly connect all of your eloquent models, mapping them to the relevant stripe entities in order to make a marketplace or payments platform. 51 | 52 | ## Documentation 53 | 54 | We now have a dedicated docs page for this plugin. You can view it [here](https://updev-1.gitbook.io/cashier-for-connect/). 55 | 56 | We now roughly support webhooks (Due to flexible nature of connect, you will need to declare handlers yourself) - Follow our guide! 57 | 58 | ## License 59 | 60 | Please refer to [LICENSE.md](https://github.com/l4nos/laravel-cashier-stripe-connect/blob/main/LICENSE) for this project's license. 61 | 62 | ## Contributors 63 | 64 | This list only contains some of the most notable contributors. For the full list, refer to [GitHub's contributors graph](https://github.com/l4nos/laravel-cashier-stripe-connect/graphs/contributors). 65 | * ExpDev07 [(Marius)](https://github.com/ExpDev07) - Creator of the original package 66 | * Haytam Bakouane [(hbakouane)](https://github.com/hbakouane) - Contributor to original package. 67 | * Robert Lane (Me) - Creator of the new package 68 | 69 | ## Thanks to 70 | 71 | [Taylor Otwell](https://twitter.com/taylorotwell) for his amazing framework and [all the contributors of Cashier](https://github.com/laravel/cashier-stripe/graphs/contributors). 72 | -------------------------------------------------------------------------------- /src/Concerns/ManagesPaymentLinks.php: -------------------------------------------------------------------------------- 1 | $lineItems 32 | ], $options); 33 | 34 | // APPLY PLATFORM FEE COMMISSION - SET THIS AGAINST THE MODEL 35 | if (isset($this->commission_type) && isset($this->commission_rate)) { 36 | if ($this->commission_type === 'percentage') { 37 | $options['application_fee_percent'] = round($this->commission_rate,2); 38 | } else { 39 | $options['application_fee_amount'] = round($this->commission_rate ,2); 40 | } 41 | } 42 | 43 | $this->assertAccountExists(); 44 | return PaymentLink::create($options, $this->stripeAccountOptions([], true)); 45 | } 46 | 47 | /** 48 | * @return PaymentLink 49 | * @throws AccountNotFoundException 50 | * @throws ApiErrorException 51 | */ 52 | public function createDestinationPaymentLink(array $lineItems, array $options, bool $onBehalfOf = false): PaymentLink{ 53 | 54 | $options = array_merge([ 55 | 'line_items' => $lineItems, 56 | 'transfer_data' => [ 57 | 'destination' => $this->stripeAccountId() 58 | ], 59 | ], $options); 60 | 61 | if($onBehalfOf){ 62 | $options['on_behalf_of'] = $this->stripeAccountId(); 63 | } 64 | 65 | // APPLY PLATFORM FEE COMMISSION - SET THIS AGAINST THE MODEL 66 | if (isset($this->commission_type) && isset($this->commission_rate)) { 67 | if ($this->commission_type === 'percentage') { 68 | $options['application_fee_percent'] = round($this->commission_rate,2); 69 | } else { 70 | $options['application_fee_amount'] = round($this->commission_rate ,2); 71 | } 72 | } 73 | 74 | $this->assertAccountExists(); 75 | return PaymentLink::create($options, $this->stripeAccountOptions([], false)); 76 | } 77 | 78 | /** 79 | * @return Collection 80 | * @throws AccountNotFoundException 81 | * @throws ApiErrorException 82 | */ 83 | public function getAllDirectPaymentLinks(): Collection{ 84 | $this->assertAccountExists(); 85 | return PaymentLink::all([], $this->stripeAccountOptions([], true)); 86 | } 87 | 88 | // NOTE, GETTING ALL THE DESTINATION PAYMENT LINKS IS IMPRACTICAL, 89 | // WE CANNOT FILTER BY CONNECTED ACCOUNT ON THE STRIPE API DIRECTLY, 90 | // I'VE REACHED OUT TO STRIPE TO ASK THEM TO ADD THAT IN 91 | 92 | /** 93 | * @return PaymentLink 94 | * @throws AccountNotFoundException 95 | * @throws ApiErrorException 96 | */ 97 | public function getSingleDestinationPaymentLink($id): PaymentLink{ 98 | $this->assertAccountExists(); 99 | return PaymentLink::retrieve($id, $this->stripeAccountOptions([], false)); 100 | } 101 | 102 | /** 103 | * @param $id 104 | * @param $data 105 | * @return PaymentLink 106 | * @throws AccountNotFoundException 107 | * @throws ApiErrorException 108 | */ 109 | public function updateDirectPaymentLink($id, $data): PaymentLink{ 110 | $this->assertAccountExists(); 111 | return PaymentLink::update($id, $data, $this->stripeAccountOptions([], true)); 112 | } 113 | 114 | /** 115 | * @param $id 116 | * @param $data 117 | * @return PaymentLink 118 | * @throws AccountNotFoundException 119 | * @throws ApiErrorException 120 | */ 121 | public function updateDestinationPaymentLink($id, $data): PaymentLink{ 122 | $this->assertAccountExists(); 123 | return PaymentLink::update($id, $data, $this->stripeAccountOptions([], false)); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Concerns/ManageCustomer.php: -------------------------------------------------------------------------------- 1 | belongsTo(ConnectCustomer::class, $this->primaryKey, $this->getLocalIDField())->where('model', '=', get_class($this)); 22 | } 23 | 24 | /** 25 | * Retrieve the Stripe account ID. 26 | * 27 | * @return string|null 28 | */ 29 | public function stripeAccountId(): ?string 30 | { 31 | return $this->stripeCustomerMapping->stripe_account_id; 32 | } 33 | 34 | /** 35 | * Retrieve the Stripe customer ID. 36 | * 37 | * @return string|null 38 | */ 39 | public function stripeCustomerId(): ?string 40 | { 41 | return $this->stripeCustomerMapping->stripe_customer_id; 42 | } 43 | 44 | /** 45 | * Checks if the model exists as a stripe customer 46 | * @return mixed 47 | */ 48 | public function hasCustomerRecord(){ 49 | return ($this->stripeCustomerMapping()->exists()); 50 | } 51 | 52 | /** 53 | * Creates a customer against a connected account, the first parameter must be a model that has 54 | * the billable trait and also exists as a stripe connected account 55 | * @param $connectedAccount 56 | * @param array $customerData 57 | * @return Customer 58 | * @throws AccountAlreadyExistsException 59 | * @throws AccountNotFoundException 60 | */ 61 | public function createStripeCustomer($connectedAccount, array $customerData = []){ 62 | 63 | // Check if model already has a connected Stripe account. 64 | if ($this->hasCustomerRecord()) { 65 | throw new AccountAlreadyExistsException('Customer account already exists.'); 66 | } 67 | 68 | $customer = Customer::create($customerData, $this->stripeAccountOptions($connectedAccount)); 69 | 70 | // Save the id. 71 | $this->stripeCustomerMapping()->create([ 72 | "stripe_customer_id" => $customer->id, 73 | "stripe_account_id" => $connectedAccount->stripeAccountId(), 74 | "model" => get_class($this), 75 | $this->getLocalIDField() => $this->{$this->primaryKey} 76 | ]); 77 | 78 | $this->save(); 79 | 80 | return $customer; 81 | 82 | } 83 | 84 | /** 85 | * Returns the parent model that the customer belongs to 86 | * You should really be relating these yourself using foreign indexes and eloquent relationships 87 | * This is only done this way for the purposes of the plugin and the dynamic mapping 88 | * @return Model 89 | */ 90 | private function retrieveHostConnectedAccount(): Model{ 91 | 92 | $connectedAccount = ConnectMapping::where([ 93 | ['stripe_account_id', '=', $this->stripeAccountId()] 94 | ])->first(); 95 | 96 | $model = $connectedAccount->model; 97 | 98 | $modelId = $this->getHostIDField($connectedAccount); 99 | 100 | return $model::find($connectedAccount->$modelId); 101 | 102 | } 103 | 104 | /** 105 | * Deletes the Stripe customer for the model. 106 | * 107 | * @return Customer 108 | * @throws AccountNotFoundException|ApiErrorException 109 | */ 110 | public function deleteStripeCustomer(): Customer 111 | { 112 | $this->assetCustomerExists(); 113 | 114 | // Process account delete. 115 | $customer = Customer::retrieve($this->stripeCustomerId(), $this->stripeAccountOptions($this->retrieveHostConnectedAccount())); 116 | $customer->delete(); 117 | 118 | // Wipe account id reference from model. 119 | $this->stripeCustomerMapping()->delete(); 120 | 121 | return $customer; 122 | } 123 | 124 | /** 125 | * Provides support for UUID based models 126 | * @return string 127 | */ 128 | private function getLocalIDField(){ 129 | 130 | if($this->incrementing){ 131 | return 'model_id'; 132 | }else{ 133 | return 'model_uuid'; 134 | } 135 | 136 | } 137 | 138 | /** 139 | * Provides support for UUID based models 140 | * @return string 141 | */ 142 | private function getHostIDField(ConnectMapping $connectedAccount){ 143 | 144 | if($connectedAccount->model_id){ 145 | return 'model_id'; 146 | }else{ 147 | return 'model_uuid'; 148 | } 149 | 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/Concerns/ManagesConnectSubscriptions.php: -------------------------------------------------------------------------------- 1 | commission_type) && isset($this->commission_rate)) { 39 | if ($this->commission_type === 'percentage') { 40 | $data['application_fee_percent'] = $this->commission_rate; 41 | } else { 42 | $data['application_fee_amount'] = $this->commission_rate; 43 | } 44 | } 45 | 46 | $customerID = $this->determineCustomerInput($customer); 47 | 48 | $subscription = Subscription::create( 49 | $data + [ 50 | "customer" => $this->determineCustomerInput($customer), 51 | "items" => [ 52 | ['price' => $price, "quantity" => $quantity] 53 | ], 54 | "payment_behavior" => "default_incomplete", 55 | "expand" => ["latest_invoice.payment_intent"] 56 | ], $this->stripeAccountOptions([], true)); 57 | 58 | // TODO REWRITE TO USE RELATIONAL CREATION 59 | // GENERATE DATABASE RECORD FOR SUBSCRIPTION 60 | $ConnectSubscriptionRecord = ConnectSubscription::create([ 61 | "name" => $name, 62 | "stripe_id" => $subscription->id, 63 | "stripe_status" => $subscription->status, 64 | "connected_price_id" => $price, 65 | "ends_at" => Date::parse($subscription->current_period_end), 66 | "stripe_customer_id" => $customerID, 67 | "stripe_account_id" => $this->stripeAccountId() 68 | ]); 69 | 70 | // TODO REWRITE TO USE RELATIONAL CREATION 71 | $ConnectSubscriptionItemRecord = ConnectSubscriptionItem::create([ 72 | "connected_subscription_id" => $ConnectSubscriptionRecord->id, 73 | "stripe_id" => $subscription->items->data[0]->id, 74 | "connected_product" => $subscription->items->data[0]->price->product, 75 | "connected_price" => $subscription->items->data[0]->price->id, 76 | "quantity" => $quantity 77 | ]); 78 | 79 | return $subscription; 80 | 81 | } 82 | 83 | /** 84 | * @return mixed 85 | */ 86 | public function getSubscriptions(){ 87 | return $this->stripeAccountMapping->subscriptions; 88 | } 89 | 90 | /** 91 | * Retrieves a subscription object by its stripe subscription ID 92 | * @param $id 93 | * @return Subscription 94 | * @throws ApiErrorException 95 | */ 96 | public function retrieveSubscriptionFromStripe($id): Subscription 97 | { 98 | return Subscription::retrieve($id, $this->stripeAccountOptions([], true)); 99 | } 100 | 101 | /** 102 | * @param $customer 103 | * @return mixed 104 | * @throws Exception 105 | */ 106 | private function determineCustomerInput($customer) 107 | { 108 | if (gettype($customer) === 'string') { 109 | return $customer; 110 | } else { 111 | return $this->handleConnectedCustomer($customer); 112 | } 113 | } 114 | 115 | /** 116 | * @param $customer 117 | * @return mixed 118 | * @throws Exception 119 | */ 120 | private function handleConnectedCustomer($customer) 121 | { 122 | // IT IS A CUSTOMER TRAIT MODEL 123 | $traits = class_uses($customer); 124 | 125 | if (!in_array('Lanos\CashierConnect\ConnectCustomer', $traits)) { 126 | throw new Exception('The '.class_basename($customer).' model does not have the connect ConnectCustomer trait.'); 127 | } 128 | 129 | $customer->assetCustomerExists(); 130 | 131 | return $customer->stripeCustomerId(); 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Concerns/ManagesAccount.php: -------------------------------------------------------------------------------- 1 | belongsTo(ConnectMapping::class, $this->primaryKey, $this->getLocalIDField())->where('model', '=', get_class($this)); 26 | } 27 | 28 | /** 29 | * Updates and returns the updated requirements against the stripe API for the mapping 30 | * @return ConnectMapping 31 | */ 32 | public function updateStripeStatus(){ 33 | 34 | $account = $this->asStripeAccount(); 35 | 36 | $onboarded = []; 37 | 38 | // IF ITS COMPLETED FIRST TIME 39 | if(!$this->stripeAccountMapping->charges_enabled && $account->charges_enabled){ 40 | $onboarded = [ 41 | "first_onboarding_done" => 1 42 | ]; 43 | } 44 | 45 | $this->stripeAccountMapping()->update([ 46 | "future_requirements" => $account->future_requirements->toArray(), 47 | "requirements" => $account->requirements->toArray(), 48 | "charges_enabled" => $account->charges_enabled 49 | ] + $onboarded); 50 | 51 | $this->refresh(); 52 | 53 | return $this->stripeAccountMapping; 54 | } 55 | 56 | /** 57 | * Retrieve the Stripe account ID. 58 | * 59 | * @return string|null 60 | */ 61 | public function stripeAccountId(): ?string 62 | { 63 | return $this->stripeAccountMapping->stripe_account_id; 64 | } 65 | 66 | /** 67 | * Determine if the entity has a Stripe account ID. 68 | * 69 | * @return bool 70 | */ 71 | public function hasStripeAccount(): bool 72 | { 73 | return ($this->stripeAccountMapping()->exists()); 74 | } 75 | 76 | /** 77 | * Gets the account email to use for Stripe. 78 | * 79 | * @return string 80 | */ 81 | public function stripeAccountEmail(): string 82 | { 83 | return $this->email; 84 | } 85 | 86 | /** 87 | * @return int 88 | */ 89 | public function getModelID(): int 90 | { 91 | return $this->{$this->primaryKey}; 92 | } 93 | 94 | /** 95 | * Determine if the entity has a Stripe account ID and throw an exception if not. 96 | * 97 | * @return void 98 | * @throws AccountNotFoundException 99 | */ 100 | protected function assertAccountExists(): void 101 | { 102 | if (! $this->hasStripeAccount()) { 103 | throw new AccountNotFoundException('Stripe account does not exist for '.class_basename(static::class).' model'); 104 | } 105 | } 106 | 107 | /** 108 | * Checks if the model has submitted their details. 109 | * 110 | * @return bool 111 | * @throws AccountNotFoundException|ApiErrorException 112 | */ 113 | public function hasSubmittedAccountDetails(): bool 114 | { 115 | $this->assertAccountExists(); 116 | 117 | return $this->asStripeAccount()->details_submitted; 118 | } 119 | 120 | /** 121 | * Checks if the model has completed on-boarding process by having submitted their details. 122 | * 123 | * @return bool 124 | * @throws AccountNotFoundException|ApiErrorException 125 | */ 126 | public function hasCompletedOnboarding() 127 | { 128 | return $this->hasSubmittedAccountDetails(); 129 | } 130 | 131 | /** 132 | * Get the Stripe account for the model. 133 | * 134 | * @return Account 135 | * @throws AccountNotFoundException|ApiErrorException 136 | */ 137 | public function asStripeAccount(): Account 138 | { 139 | $this->assertAccountExists(); 140 | 141 | return Account::retrieve($this->stripeAccountId(), $this->stripeAccountOptions()); 142 | } 143 | 144 | /** 145 | * Create a Stripe account for the given model. 146 | * 147 | * @param string $type 148 | * @param array $options 149 | * @return Account 150 | * @throws AccountAlreadyExistsException|ApiErrorException 151 | */ 152 | public function createAsStripeAccount(string $type = 'standard', array $options = []): Account 153 | { 154 | // Check if model already has a connected Stripe account. 155 | if ($this->hasStripeAccount()) { 156 | throw new AccountAlreadyExistsException('Stripe account already exists.'); 157 | } 158 | 159 | // Create payload. 160 | $options = array_merge([ 161 | 'type' => $type, 162 | 'email' => $this->stripeAccountEmail(), 163 | ], $options); 164 | 165 | // Create account. 166 | $account = Account::create($options, $this->stripeAccountOptions()); 167 | 168 | // Save the id. 169 | $this->stripeAccountMapping()->create([ 170 | "stripe_account_id" => $account->id, 171 | "charges_enabled" => $account->charges_enabled, 172 | "future_requirements" => $account->future_requirements, 173 | "type" => $type, 174 | "requirements" => $account->requirements, 175 | "model" => get_class($this), 176 | $this->getLocalIDField() => $this->{$this->primaryKey} 177 | ]); 178 | 179 | $this->save(); 180 | 181 | return $account; 182 | } 183 | 184 | /** 185 | * Get the Stripe account instance for the current model or create one. 186 | * 187 | * @param string $type 188 | * @param array $options 189 | * @return Account 190 | * @throws AccountNotFoundException|AccountAlreadyExistsException|ApiErrorException 191 | */ 192 | public function createOrGetStripeAccount(string $type = 'express', array $options = []): Account 193 | { 194 | // Return Stripe account if exists, otherwise create new. 195 | return $this->hasStripeAccount() 196 | ? $this->asStripeAccount() 197 | : $this->createAsStripeAccount($type, $options); 198 | } 199 | 200 | /** 201 | * Deletes the Stripe account for the model. 202 | * 203 | * @return Account 204 | * @throws AccountNotFoundException|ApiErrorException 205 | */ 206 | public function deleteStripeAccount(): Account 207 | { 208 | $this->assertAccountExists(); 209 | 210 | // Process account delete. 211 | $account = $this->asStripeAccount(); 212 | $account->delete(); 213 | 214 | // Wipe account id reference from model. 215 | $this->stripeAccountMapping()->delete(); 216 | 217 | return $account; 218 | } 219 | 220 | /** 221 | * Deletes the Stripe account if it exists and re-creates it. 222 | * 223 | * @param string $type 224 | * @param array $options 225 | * @return Account 226 | * @throws AccountNotFoundException|AccountAlreadyExistsException|ApiErrorException 227 | */ 228 | public function deleteAndCreateStripeAccount(string $type = 'express', array $options = []): Account 229 | { 230 | // Delete account if it already exists. 231 | if ($this->hasStripeAccount()) { 232 | $this->deleteStripeAccount(); 233 | } 234 | 235 | // Create account and return it. 236 | return $this->createAsStripeAccount($type, $options); 237 | } 238 | 239 | /** 240 | * Update the underlying Stripe account information for the model. 241 | * 242 | * @param array $options 243 | * @return Account 244 | * @throws AccountNotFoundException|ApiErrorException 245 | */ 246 | public function updateStripeAccount(array $options = []): Account 247 | { 248 | $this->assertAccountExists(); 249 | 250 | $accountUpdate = Account::update($this->stripeAccountId(), $options, $this->stripeAccountOptions()); 251 | 252 | // UPDATE ANY FLAGS OR REQUIREMENTS 253 | // TODO ADD PAYOUTS ENABLED FLAG 254 | $mapping = $this->stripeAccountMapping()->update([ 255 | "future_requirements" => $accountUpdate->future_requirements->toArray(), 256 | "requirements" => $accountUpdate->requirements->toArray(), 257 | "charges_enabled" => $accountUpdate->charges_enabled 258 | ]); 259 | 260 | return $accountUpdate; 261 | } 262 | 263 | /** 264 | * @return string 265 | */ 266 | private function getLocalIDField(){ 267 | 268 | if($this->getIncrementing()){ 269 | return 'model_id'; 270 | }else{ 271 | return 'model_uuid'; 272 | } 273 | 274 | } 275 | 276 | } 277 | --------------------------------------------------------------------------------