├── .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 |