├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── stripe.php ├── database ├── factories │ ├── StripeAccountFactory.php │ └── StripeEventFactory.php └── migrations │ └── 2019_07_17_074500_create_stripe_accounts_and_events.php ├── docs ├── connect.md ├── console.md ├── installation.md ├── repositories.md ├── testing.md └── webhooks.md ├── phpunit.xml ├── resources └── brand │ ├── badge │ ├── big │ │ ├── powered_by_stripe.pdf │ │ ├── powered_by_stripe.png │ │ ├── powered_by_stripe.svg │ │ ├── powered_by_stripe@2x.png │ │ └── powered_by_stripe@3x.png │ ├── outline-dark │ │ ├── powered_by_stripe.pdf │ │ ├── powered_by_stripe.png │ │ ├── powered_by_stripe.svg │ │ ├── powered_by_stripe@2x.png │ │ └── powered_by_stripe@3x.png │ ├── outline-light │ │ ├── powered_by_stripe.pdf │ │ ├── powered_by_stripe.png │ │ ├── powered_by_stripe.svg │ │ ├── powered_by_stripe@2x.png │ │ └── powered_by_stripe@3x.png │ ├── solid-dark │ │ ├── powered_by_stripe.pdf │ │ ├── powered_by_stripe.png │ │ ├── powered_by_stripe.svg │ │ ├── powered_by_stripe@2x.png │ │ └── powered_by_stripe@3x.png │ └── solid-light │ │ ├── powered_by_stripe.pdf │ │ ├── powered_by_stripe.png │ │ ├── powered_by_stripe.svg │ │ ├── powered_by_stripe@2x.png │ │ └── powered_by_stripe@3x.png │ └── connect-button │ ├── blue │ ├── blue-on-dark.png │ ├── blue-on-dark@2x.png │ ├── blue-on-dark@3x.png │ ├── blue-on-light.png │ ├── blue-on-light@2x.png │ └── blue-on-light@3x.png │ └── gray │ ├── light-on-dark.png │ ├── light-on-dark@2x.png │ ├── light-on-dark@3x.png │ ├── light-on-light.png │ ├── light-on-light@2x.png │ └── light-on-light@3x.png ├── src ├── Assert.php ├── Client.php ├── Config.php ├── Connect │ ├── Adapter.php │ ├── AuthorizeUrl.php │ ├── Authorizer.php │ ├── ConnectedAccount.php │ ├── Connector.php │ ├── OwnsStripeAccounts.php │ └── SessionState.php ├── Connector.php ├── Console │ └── Commands │ │ └── StripeQuery.php ├── Contracts │ ├── Connect │ │ ├── AccountInterface.php │ │ ├── AccountOwnerInterface.php │ │ ├── AdapterInterface.php │ │ └── StateProviderInterface.php │ └── Webhooks │ │ └── ProcessorInterface.php ├── Events │ ├── AbstractOAuthEvent.php │ ├── AccountDeauthorized.php │ ├── ClientReceivedResult.php │ ├── ClientWillSend.php │ ├── FetchedUserCredentials.php │ ├── OAuthError.php │ ├── OAuthSuccess.php │ └── SignatureVerificationFailed.php ├── Exceptions │ ├── AccountNotConnectedException.php │ └── UnexpectedValueException.php ├── Facades │ └── Stripe.php ├── Http │ ├── Controllers │ │ ├── OAuthController.php │ │ └── WebhookController.php │ ├── Middleware │ │ └── VerifySignature.php │ └── Requests │ │ └── AuthorizeConnect.php ├── Jobs │ ├── FetchUserCredentials.php │ └── ProcessWebhook.php ├── LaravelStripe.php ├── Listeners │ ├── DispatchAuthorizeJob.php │ ├── DispatchWebhookJob.php │ ├── LogClientRequests.php │ ├── LogClientResults.php │ └── RemoveAccountOnDeauthorize.php ├── Log │ └── Logger.php ├── Models │ ├── StripeAccount.php │ └── StripeEvent.php ├── Repositories │ ├── AbstractRepository.php │ ├── AccountRepository.php │ ├── BalanceRepository.php │ ├── ChargeRepository.php │ ├── Concerns │ │ ├── All.php │ │ ├── HasMetadata.php │ │ ├── Retrieve.php │ │ └── Update.php │ ├── EventRepository.php │ ├── PaymentIntentRepository.php │ └── RefundRepository.php ├── ServiceProvider.php ├── StripeService.php ├── Testing │ ├── ClientFake.php │ ├── Concerns │ │ └── MakesStripeAssertions.php │ └── StripeFake.php └── Webhooks │ ├── ConnectWebhook.php │ ├── Processor.php │ ├── Verifier.php │ └── Webhook.php └── tests ├── database ├── factories │ └── TestFactory.php └── migrations │ ├── 2014_10_12_000000_create_users_table.php │ └── 2019_07_16_000000_create_test_tables.php ├── lib ├── Integration │ ├── Connect │ │ ├── AuthorizeTest.php │ │ ├── DeauthorizeTest.php │ │ └── OAuthTest.php │ ├── Console │ │ └── StripeQueryTest.php │ ├── EloquentTest.php │ ├── TestCase.php │ └── Webhooks │ │ ├── ListenersTest.php │ │ ├── ProcessTest.php │ │ ├── ReceiveTest.php │ │ └── WebhookTest.php ├── TestAccount.php ├── TestUser.php ├── TestWebhookJob.php └── Unit │ └── Connect │ └── AuthorizeUrlTest.php ├── resources └── views │ └── oauth │ ├── error.blade.php │ └── success.blade.php └── stubs └── webhook.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{php,json}] 11 | indent_size = 4 12 | 13 | [*.{md,yml}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | php: [8.1, 8.2] 18 | laravel: [10] 19 | 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v3 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd 29 | tools: composer:v2 30 | coverage: none 31 | ini-values: error_reporting=E_ALL 32 | 33 | - name: Set Laravel Version 34 | run: composer require "laravel/framework:^${{ matrix.laravel }}" --no-update 35 | 36 | - name: Install dependencies 37 | uses: nick-fields/retry@v2 38 | with: 39 | timeout_minutes: 5 40 | max_attempts: 5 41 | command: composer update --prefer-dist --no-interaction --no-progress 42 | 43 | - name: Execute tests 44 | run: vendor/bin/phpunit 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to 4 | [Semantic Versioning](http://semver.org/) and [this changelog format](http://keepachangelog.com/). 5 | 6 | ## Unreleased 7 | 8 | ### Removed 9 | 10 | - Removed checking the prefix of account and charge ids, as Stripe does not consider changing these as 11 | a [breaking change.](https://docs.stripe.com/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible) 12 | This was causing issues in the refund repository, as it was expecting a charge id starting `ch_`. However, Stripe now 13 | also uses `py_` for some refundable payments. 14 | 15 | ## [0.7.0] - 2023-03-19 16 | 17 | ### Changed 18 | 19 | - Minimum PHP version is now 8.1. 20 | - Upgraded to Laravel 10, dropping support for Laravel 8 and 9. 21 | 22 | ## [0.6.0] - 2022-02-18 23 | 24 | ### Added 25 | 26 | - Package now supports Laravel 9. 27 | 28 | ### Changed 29 | 30 | - Minimum PHP version is now PHP 7.4. 31 | 32 | ## [0.5.2] - 2022-02-18 33 | 34 | ### Fixed 35 | 36 | - [#12](https://github.com/cloudcreativity/laravel-stripe/issues/12) Fixed oAuth process note returning a scope for a 37 | Stripe Express account. 38 | 39 | ## [0.5.1] - 2021-03-17 40 | 41 | ### Added 42 | 43 | - Package now supports PHP 8 (in addition to `^7.3`). 44 | 45 | ## [0.5.0] - 2020-09-09 46 | 47 | ### Changed 48 | 49 | - Minimum PHP version is now 7.3. 50 | - Minimum Laravel version is now 8.0. 51 | 52 | ## [0.4.0] - 2020-09-09 53 | 54 | ### Added 55 | 56 | - Added balance repository. 57 | - The `stripe:query` Artisan command now accepts resource names in either singular or plural form. 58 | 59 | ### Fixed 60 | 61 | - **BREAKING:** The Stripe accounts relationship on the `Connect\OwnsStripeAccounts` trait now correctly uses 62 | the `Contracts\Connect\AccountOwnerInterface::getStripeIdentifierName()` method to determine the column name on the 63 | inverse model. This means the column name now defaults to `owner_id`. This change could potentially break 64 | implementations. If you use a different column from `owner_id`, then overload the `getStripeIdentifierName()` method 65 | on the model that owns Stripe accounts. 66 | - Fixed catching API exceptions in the `stripe:query` Artisan command. 67 | 68 | ## [0.3.0] - 2020-07-27 69 | 70 | ### Changed 71 | 72 | - Minimum PHP version is now `7.2.5`. 73 | - Minimum Laravel version is now `7.x`. 74 | - Minimum Stripe PHP version is now `7.0`. 75 | 76 | ## [0.2.0] - 2020-06-17 77 | 78 | Release for Laravel `5.5`, `5.6`, `5.7`, `5.8` and `6.x`. 79 | 80 | ## [0.1.1] - 2020-01-04 81 | 82 | ### Fixed 83 | 84 | - [#3](git@github.com:cloudcreativity/laravel-stripe.git) 85 | Fix facade alias in Composer json. 86 | 87 | ## [0.1.0] - 2019-08-12 88 | 89 | Initial release for PHP 5.6 / Laravel 5.4. 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Stripe 2 | 3 | ## Status 4 | 5 | **This package no longer has active support beyond upgrading to the latest Laravel version. Please note however, that we 6 | cannot guarantee that we will be able to maintain support for all Laravel versions going forward. The package is also on 7 | an old version of the Stripe SDK which limits its usefulness.** 8 | 9 | Unfortunately due to only having limited time for open source work, we are unable to maintain this package to the 10 | standard we would like. We would however accept pull requests from anyone who does want to contribute upgrades or new 11 | features. 12 | 13 | However, if you are starting a new project it is probably best not to use this package. 14 | 15 | ## Overview 16 | 17 | A Laravel integration for [Stripe's official PHP package.](https://github.com/stripe/stripe-php) 18 | 19 | This package allows you to fluently query the Stripe API via repositories. 20 | Repositories can be for either your application's Stripe account, or connected Stripe accounts. 21 | 22 | ### Example 23 | 24 | ```php 25 | // For your application's account: 26 | /** @var \Stripe\PaymentIntent $intent */ 27 | $intent = Stripe::account() 28 | ->paymentIntents() 29 | ->create('gbp', 1500); 30 | 31 | // For a Stripe Connect account model: 32 | $account->stripe()->paymentIntents()->create('gbp', 999); 33 | ``` 34 | 35 | ### What About Cashier? 36 | 37 | This package is meant to be used *in addition* to [Laravel Cashier](https://laravel.com/docs/billing), 38 | not instead of it. 39 | 40 | Our primary use-case is Stripe Connect. We needed a package that provided really easy access to data from 41 | connected Stripe accounts. We wanted to make interacting with the entire Stripe API fluent, 42 | easily testable and highly debuggable. 43 | 44 | In contrast, Cashier does not provide full Stripe API coverage, and provides 45 | [no support for Stripe Connect.](https://github.com/laravel/cashier/pull/519) 46 | So if you need to do more than just Cashier's billing functionality, install this package as well. 47 | 48 | ## Installation 49 | 50 | Installation is via Composer. Refer to the [Installation Guide](./docs/installation.md) for 51 | instructions. 52 | 53 | ## Documentation 54 | 55 | 1. [Installation](./docs/installation.md) 56 | 2. [Accessing the Stripe API](./docs/repositories.md) 57 | 3. [Receiving Webhooks](./docs/webhooks.md) 58 | 4. [Stripe Connect](./docs/connect.md) 59 | 5. [Artisan Commands](./docs/console.md) 60 | 6. [Testing](./docs/testing.md) 61 | 62 | ## Version Compatibility 63 | 64 | The following table shows which version to install. We have provided the Stripe API version that we 65 | developed against as guide. You may find the package works with older versions of the API. 66 | 67 | | Laravel | Stripe PHP | Stripe API | Laravel-Stripe | Cashier | 68 | |:--------|:-----------|:---------------|:---------------|:----------------------------| 69 | | `10.x` | `^7.52` | `>=2020-03-02` | `0.7.x` | `^14.8` | 70 | | `9.x` | `^7.52` | `>=2020-03-02` | `0.6.x` | `^12.3` | 71 | | `8.x` | `^7.52` | `>=2020-03-02` | `0.5.x\|0.6.x` | `^12.3` | 72 | | `7.x` | `^7.0` | `>=2020-03-02` | `0.4.x` | `^12.0` | 73 | | `6.x` | `^6.40` | `>=2019-05-16` | `0.2.x` | `^9.0\|^10.0\|^11.0\|^12.0` | 74 | 75 | ## Contributing 76 | 77 | We have only implemented the repositories for the Stripe resources we are using in our application. 78 | Repositories are very easy to implement - for example, the 79 | [payment intent repository](./src/Repositories/PaymentIntentRepository.php) - 80 | because they are predominantly composed of traits. Then they just need to be added to 81 | [the connector class](./src/Connector.php). 82 | 83 | If you find this package is missing a resource you need in your application, an ideal way to contribute 84 | is to submit a pull request to add the missing repository. 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudcreativity/laravel-stripe", 3 | "description": "Laravel integration for Stripe, including Stripe Connect.", 4 | "type": "library", 5 | "license": "Apache-2.0", 6 | "authors": [ 7 | { 8 | "name": "Christopher Gammie", 9 | "email": "info@cloudcreativity.co.uk" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "prefer-stable": true, 14 | "require": { 15 | "php": "^8.1", 16 | "ext-json": "*", 17 | "illuminate/console": "^10.0", 18 | "illuminate/contracts": "^10.0", 19 | "illuminate/database": "^10.0", 20 | "illuminate/http": "^10.0", 21 | "illuminate/queue": "^10.0", 22 | "illuminate/routing": "^10.0", 23 | "illuminate/support": "^10.0", 24 | "psr/log": "^3.0", 25 | "stripe/stripe-php": "^7.52" 26 | }, 27 | "require-dev": { 28 | "laravel/cashier": "^14.8", 29 | "laravel/legacy-factories": "^1.0", 30 | "orchestra/testbench": "^8.0", 31 | "phpunit/phpunit": "^9.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "CloudCreativity\\LaravelStripe\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "CloudCreativity\\LaravelStripe\\Tests\\": "tests/lib" 41 | } 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-develop": "1.x-dev" 46 | }, 47 | "laravel": { 48 | "providers": [ 49 | "CloudCreativity\\LaravelStripe\\ServiceProvider" 50 | ], 51 | "aliases": { 52 | "Stripe": "CloudCreativity\\LaravelStripe\\Facades\\Stripe" 53 | } 54 | } 55 | }, 56 | "config": { 57 | "sort-packages": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /database/factories/StripeAccountFactory.php: -------------------------------------------------------------------------------- 1 | define(StripeAccount::class, function (Faker $faker) { 26 | return [ 27 | 'id' => $faker->unique()->lexify('acct_????????????????'), 28 | 'country' => $faker->randomElement(['AU', 'GB', 'US']), 29 | 'default_currency' => $faker->randomElement(Config::currencies()->all()), 30 | 'details_submitted' => $faker->boolean(75), 31 | 'email' => $faker->email(), 32 | 'payouts_enabled' => $faker->boolean(75), 33 | 'type' => $faker->randomElement(['standard', 'express', 'custom']), 34 | ]; 35 | }); 36 | -------------------------------------------------------------------------------- /database/factories/StripeEventFactory.php: -------------------------------------------------------------------------------- 1 | define(StripeEvent::class, function (Faker $faker) { 26 | return [ 27 | 'id' => $faker->unique()->lexify('evt_????????????????'), 28 | 'api_version' => $faker->date(), 29 | 'created' => $faker->dateTimeBetween('-1 hour', 'now'), 30 | 'livemode' => $faker->boolean(), 31 | 'pending_webhooks' => $faker->numberBetween(0, 100), 32 | 'type' => $faker->randomElement([ 33 | 'charge.failed', 34 | 'payment_intent.succeeded', 35 | ]), 36 | ]; 37 | }); 38 | 39 | $factory->state(StripeEvent::class, 'connect', function () { 40 | return [ 41 | 'account_id' => factory(StripeAccount::class), 42 | ]; 43 | }); 44 | -------------------------------------------------------------------------------- /database/migrations/2019_07_17_074500_create_stripe_accounts_and_events.php: -------------------------------------------------------------------------------- 1 | string('id', 255)->primary()->collate(LaravelStripe::ID_DATABASE_COLLATION); 35 | $table->timestamps(); 36 | $table->softDeletes(); 37 | $table->json('business_profile')->nullable(); 38 | $table->string('business_type')->nullable(); 39 | $table->json('capabilities')->nullable(); 40 | $table->boolean('charges_enabled')->nullable(); 41 | $table->json('company')->nullable(); 42 | $table->string('country', 3)->nullable(); 43 | $table->timestamp('created')->nullable(); 44 | $table->string('default_currency', 3)->nullable(); 45 | $table->boolean('details_submitted')->nullable(); 46 | $table->string('email')->nullable(); 47 | $table->json('individual')->nullable(); 48 | $table->json('metadata')->nullable(); 49 | $table->unsignedInteger('owner_id')->nullable(); 50 | $table->boolean('payouts_enabled')->nullable(); 51 | $table->string('refresh_token')->nullable(); 52 | $table->json('requirements')->nullable(); 53 | $table->json('settings')->nullable(); 54 | $table->string('token_scope')->nullable(); 55 | $table->json('tos_acceptance')->nullable(); 56 | $table->string('type')->nullable(); 57 | }); 58 | 59 | Schema::create('stripe_events', function (Blueprint $table) { 60 | $table->string('id', 255)->primary()->collate(LaravelStripe::ID_DATABASE_COLLATION); 61 | $table->timestamps(); 62 | $table->string('account_id', 255)->nullable()->collate(LaravelStripe::ID_DATABASE_COLLATION); 63 | $table->date('api_version'); 64 | $table->timestamp('created'); 65 | $table->boolean('livemode'); 66 | $table->unsignedInteger('pending_webhooks'); 67 | $table->string('type'); 68 | $table->json('request')->nullable(); 69 | }); 70 | } 71 | 72 | /** 73 | * Reverse the migration. 74 | * 75 | * @return void 76 | */ 77 | public function down() 78 | { 79 | Schema::dropIfExists('stripe_events'); 80 | Schema::dropIfExists('stripe_accounts'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/console.md: -------------------------------------------------------------------------------- 1 | # Console 2 | 3 | ## `stripe:query` 4 | 5 | You can query Stripe resources using the `stripe:query` Artisan command. 6 | For example, to query charges on your application's account: 7 | 8 | ```bash 9 | $ php artisan stripe:query charge 10 | ``` 11 | 12 | Or to query a specific charge on a connected account: 13 | 14 | ```bash 15 | $ php artisan stripe:query charge ch_4X8JtIYiSwHJ0o --account=acct_hrGMqodSZxqRuTM1 16 | ``` 17 | 18 | The options available are: 19 | 20 | ``` 21 | Usage: 22 | stripe:query [options] [--] [] 23 | 24 | Arguments: 25 | resource The resource name 26 | id The resource id 27 | 28 | Options: 29 | -A, --account[=ACCOUNT] The connected account 30 | -e, --expand[=EXPAND] The paths to expand (multiple values allowed) 31 | ``` 32 | 33 | > This command is provided for debugging data in your Stripe API. 34 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Install the package using Composer: 4 | 5 | ```bash 6 | $ composer require cloudcreativity/laravel-stripe:1.x-dev 7 | ``` 8 | 9 | Add the service provider and facade to your app config file: 10 | 11 | ```php 12 | // config/app.php 13 | return [ 14 | // ... 15 | 16 | 'providers' => [ 17 | // ... 18 | CloudCreativity\LaravelStripe\ServiceProvider::class, 19 | ], 20 | 21 | 'aliases' => [ 22 | // ... 23 | 'Stripe' => CloudCreativity\LaravelStripe\Facades\Stripe::class, 24 | ], 25 | ]; 26 | ``` 27 | 28 | Then publish the package config: 29 | 30 | ```bash 31 | $ php artisan vendor:publish --tag=stripe 32 | ``` 33 | 34 | ## Configuration 35 | 36 | Package configuration is in the `stripe.php` config file. That file contains descriptions of 37 | each configuration option, and these options are also referred to in the relevant documentation 38 | chapters. 39 | 40 | Note that by default Laravel puts your Stripe keys in the `services` config file. We expect 41 | them to be there too. Here's an example from Laravel 5.8: 42 | 43 | ```php 44 | return [ 45 | // ...other service config 46 | 47 | 'stripe' => [ 48 | 'model' => \App\User::class, 49 | 'key' => env('STRIPE_KEY'), 50 | 'secret' => env('STRIPE_SECRET'), 51 | 'webhook' => [ 52 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 53 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 54 | ], 55 | ], 56 | ]; 57 | ``` 58 | 59 | ## Migrations 60 | 61 | This package contains a number of migrations for the models it provides. **By default these 62 | are loaded by the package.** 63 | 64 | If you are customising any of the models in our implementation, you will need to disable migrations 65 | and publish the migrations instead. 66 | 67 | First, disable the migrations in the `register()` method of your application's service provider: 68 | 69 | ```php 70 | namespace App\Providers; 71 | 72 | use CloudCreativity\LaravelStripe\LaravelStripe; 73 | use Illuminate\Support\ServiceProvider; 74 | 75 | class AppServiceProvider extends ServiceProvider 76 | { 77 | 78 | public function register() 79 | { 80 | LaravelStripe::withoutMigrations(); 81 | } 82 | } 83 | ``` 84 | 85 | Then publish the migrations: 86 | 87 | ```bash 88 | $ php artisan vendor:publish --tag=stripe-migrations 89 | ``` 90 | 91 | > You must disable migrations **before** attempting to publish them, as they will only be publishable 92 | if migrations are disabled. Plus you must use the `register()` method, not `boot()`. 93 | 94 | ## Brand Assets 95 | 96 | If you want to use *Powered by Stripe* badges, or *Connect with Stripe* buttons, publish 97 | [Stripe brand assets](https://stripe.com/gb/newsroom/brand-assets) using the following command: 98 | 99 | ```bash 100 | $ php artisan vendor:publish --tag=stripe-brand 101 | ``` 102 | 103 | This will publish the files into the `public/vendor/stripe/brand` folder. 104 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Test calls to the Stripe API via our test helpers. 4 | 5 | ## Usage 6 | 7 | You may use the Stripe facade's `fake()` method to prevent all static calls to the 8 | `stripe/stripe-php` library from executing. This prevents any requests being sent to Stripe 9 | in your tests. 10 | 11 | You can then assert that static calls were made and even inspect the arguments they received. 12 | For this to work, you need to tell the fake what objects to return (and in what order) 13 | **before** the code under test is executed, and then make the assertions **after** the 14 | test code is executed. 15 | 16 | For example: 17 | 18 | ```php 19 | namespace Tests\Feature; 20 | 21 | use Tests\TestCase; 22 | use CloudCreativity\LaravelStripe\Facades\Stripe; 23 | 24 | class StripeTest extends TestCase 25 | { 26 | 27 | public function test() 28 | { 29 | Stripe::fake( 30 | $expected = new \Stripe\PaymentIntent() 31 | ); 32 | 33 | $account = factory(StripeAccount::class)->create(); 34 | $actual = $account->stripe()->paymentIntents()->create('gbp', 999); 35 | 36 | $this->assertSame($expected, $actual); 37 | 38 | Stripe::assertInvoked( 39 | \Stripe\PaymentIntent::class, 40 | 'create', 41 | function ($params, $options) use ($account) { 42 | $this->assertEquals(['currency' => 'gbp', 'amount' => 999], $params); 43 | $this->assertEquals(['stripe_account' => $account->id], $options); 44 | return true; 45 | } 46 | ); 47 | } 48 | } 49 | ``` 50 | 51 | If you are expecting multiple calls, you can queue up multiple return results: 52 | 53 | ```php 54 | Stripe::fake( 55 | new \Stripe\Account(), 56 | new \Stripe\Charge() 57 | ) 58 | ``` 59 | 60 | In this scenario, you need to call `assertInvoked()` in *exactly* the same order 61 | as you were expecting the static calls to be made. 62 | 63 | ## Asserting No Calls 64 | 65 | The Stripe fake fails the test if it is called when it no longer has any queued 66 | results. This means that if you expect Stripe to never be called, all you need 67 | to do is: 68 | 69 | ```php 70 | Stripe::fake() 71 | ``` 72 | 73 | In this scenario, if there is an unexpected call the test will fail. 74 | 75 | ## Non-Static Methods 76 | 77 | Calling `Stripe::fake()` only prevents **static** methods in Stripe's PHP package from being 78 | called. This means you will need to mock any non-static methods. 79 | 80 | For example, it is possible to cancel a payment intent by calling the `cancel()` method 81 | on a `\Stripe\PaymentIntent` instance. To test this, we will need to provide a mock 82 | as the static return result: 83 | 84 | ```php 85 | // Example using PHPUnit mock... 86 | $mock = $this 87 | ->getMockBuilder(\Stripe\PaymentIntent::class) 88 | ->setMethods(['cancel']) 89 | ->getMock(); 90 | 91 | $mock->expects($this->once())->method('cancel'); 92 | 93 | Stripe::fake($mock); 94 | 95 | // ...run test code. 96 | ``` 97 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | src/ 21 | 22 | 23 | 24 | 25 | ./tests/lib/Unit/ 26 | 27 | 28 | ./tests/lib/Integration/ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /resources/brand/badge/big/powered_by_stripe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/big/powered_by_stripe.pdf -------------------------------------------------------------------------------- /resources/brand/badge/big/powered_by_stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/big/powered_by_stripe.png -------------------------------------------------------------------------------- /resources/brand/badge/big/powered_by_stripe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/big/powered_by_stripe@2x.png -------------------------------------------------------------------------------- /resources/brand/badge/big/powered_by_stripe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/big/powered_by_stripe@3x.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-dark/powered_by_stripe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-dark/powered_by_stripe.pdf -------------------------------------------------------------------------------- /resources/brand/badge/outline-dark/powered_by_stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-dark/powered_by_stripe.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-dark/powered_by_stripe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-dark/powered_by_stripe@2x.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-dark/powered_by_stripe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-dark/powered_by_stripe@3x.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-light/powered_by_stripe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-light/powered_by_stripe.pdf -------------------------------------------------------------------------------- /resources/brand/badge/outline-light/powered_by_stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-light/powered_by_stripe.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-light/powered_by_stripe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-light/powered_by_stripe@2x.png -------------------------------------------------------------------------------- /resources/brand/badge/outline-light/powered_by_stripe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/outline-light/powered_by_stripe@3x.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-dark/powered_by_stripe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-dark/powered_by_stripe.pdf -------------------------------------------------------------------------------- /resources/brand/badge/solid-dark/powered_by_stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-dark/powered_by_stripe.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-dark/powered_by_stripe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-dark/powered_by_stripe@2x.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-dark/powered_by_stripe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-dark/powered_by_stripe@3x.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-light/powered_by_stripe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-light/powered_by_stripe.pdf -------------------------------------------------------------------------------- /resources/brand/badge/solid-light/powered_by_stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-light/powered_by_stripe.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-light/powered_by_stripe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-light/powered_by_stripe@2x.png -------------------------------------------------------------------------------- /resources/brand/badge/solid-light/powered_by_stripe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/badge/solid-light/powered_by_stripe@3x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-dark.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-dark@2x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-dark@3x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-light.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-light@2x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/blue/blue-on-light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/blue/blue-on-light@3x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-dark.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-dark@2x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-dark@3x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-light.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-light@2x.png -------------------------------------------------------------------------------- /resources/brand/connect-button/gray/light-on-light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/resources/brand/connect-button/gray/light-on-light@3x.png -------------------------------------------------------------------------------- /src/Assert.php: -------------------------------------------------------------------------------- 1 | containsStrict($currency)) { 60 | throw new UnexpectedValueException("Expecting a valid currency, received: {$currency}"); 61 | } 62 | } 63 | 64 | /** 65 | * Assert that the currency and amount are chargeable. 66 | * 67 | * @param $currency 68 | * @param $amount 69 | * @return void 70 | * @see https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts 71 | */ 72 | public static function chargeAmount($currency, $amount) 73 | { 74 | self::supportedCurrency($currency); 75 | 76 | if (!is_int($amount)) { 77 | throw new UnexpectedValueException('Expecting an integer.'); 78 | } 79 | 80 | $minimum = Config::minimum($currency); 81 | 82 | if ($minimum > $amount) { 83 | throw new UnexpectedValueException("Expecting a charge amount that is greater than {$minimum}."); 84 | } 85 | } 86 | 87 | /** 88 | * Assert that the value is a zero-decimal amount. 89 | * 90 | * @param $amount 91 | * @return void 92 | * @see https://stripe.com/docs/currencies#zero-decimal 93 | */ 94 | public static function zeroDecimal($amount) 95 | { 96 | if (!is_int($amount) || 0 > $amount) { 97 | throw new UnexpectedValueException('Expecting a positive integer.'); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | events = $events; 42 | } 43 | 44 | /** 45 | * @param string $class 46 | * @param string $method 47 | * @param mixed ...$args 48 | * @return mixed 49 | */ 50 | public function __invoke($class, $method, ...$args) 51 | { 52 | if (!is_callable("{$class}::{$method}")) { 53 | throw new InvalidArgumentException(sprintf('Cannot class %s method %s', $class, $method)); 54 | } 55 | 56 | $name = Str::snake(class_basename($class)); 57 | 58 | $this->events->dispatch(new ClientWillSend($name, $method, $args)); 59 | 60 | $result = $this->execute($class, $method, $args); 61 | 62 | $this->events->dispatch(new ClientReceivedResult($name, $method, $args, $result)); 63 | 64 | return $result; 65 | } 66 | 67 | /** 68 | * Execute the static Stripe call. 69 | * 70 | * @param $class 71 | * @param $method 72 | * @param array $args 73 | * @return mixed 74 | */ 75 | protected function execute($class, $method, array $args) 76 | { 77 | return call_user_func_array("{$class}::{$method}", $args); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Connect/Adapter.php: -------------------------------------------------------------------------------- 1 | model = $model; 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function find($accountId) 55 | { 56 | return $this->query($accountId)->first(); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function store($accountId, $refreshToken, $scope, AccountOwnerInterface $owner) 63 | { 64 | $account = $this->findWithTrashed($accountId) ?: $this->newInstance($accountId); 65 | $account->{$this->model->getStripeRefreshTokenName()} = $refreshToken; 66 | $account->{$this->model->getStripeTokenScopeName()} = $scope; 67 | $account->{$this->model->getStripeOwnerIdentifierName()} = $owner->getStripeIdentifier(); 68 | 69 | if ($account->exists && $this->softDeletes()) { 70 | $account->restore(); 71 | } else { 72 | $account->save(); 73 | } 74 | 75 | return $account; 76 | } 77 | 78 | /** 79 | * @inheritDoc 80 | */ 81 | public function update(AccountInterface $account, Account $resource) 82 | { 83 | if (!$account instanceof $this->model) { 84 | throw new UnexpectedValueException('Unexpected Stripe account model.'); 85 | } 86 | 87 | if ($account->getStripeAccountIdentifier() !== $resource->id) { 88 | throw new UnexpectedValueException('Unexpected Stripe account resource.'); 89 | } 90 | 91 | $account->update($resource->jsonSerialize()); 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | public function remove(AccountInterface $account) 98 | { 99 | if (!$account instanceof $this->model) { 100 | throw new UnexpectedValueException('Unexpected Stripe account model.'); 101 | } 102 | 103 | $account->{$this->model->getStripeRefreshTokenName()} = null; 104 | $account->{$this->model->getStripeTokenScopeName()} = null; 105 | $account->save(); 106 | 107 | if ($this->softDeletes()) { 108 | $account->delete(); 109 | } 110 | } 111 | 112 | /** 113 | * @param $accountId 114 | * @return Builder 115 | */ 116 | protected function query($accountId) 117 | { 118 | return $this->model->newQuery()->where( 119 | $this->model->getStripeAccountIdentifierName(), 120 | $accountId 121 | ); 122 | } 123 | 124 | /** 125 | * @param $accountId 126 | * @return Model|null 127 | */ 128 | protected function findWithTrashed($accountId) 129 | { 130 | $query = $this->query($accountId); 131 | 132 | if ($this->softDeletes()) { 133 | $query->withTrashed(); 134 | } 135 | 136 | return $query->first(); 137 | } 138 | 139 | /** 140 | * Make a new instance of the account model. 141 | * 142 | * @param $accountId 143 | * @return Model 144 | */ 145 | protected function newInstance($accountId) 146 | { 147 | $account = $this->model->newInstance(); 148 | $account->{$this->model->getStripeAccountIdentifierName()} = $accountId; 149 | 150 | return $account; 151 | } 152 | 153 | /** 154 | * Does the model soft-delete? 155 | * 156 | * @return bool 157 | */ 158 | protected function softDeletes() 159 | { 160 | return method_exists($this->model, 'forceDelete'); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/Connect/Authorizer.php: -------------------------------------------------------------------------------- 1 | client = $client; 55 | $this->state = $state; 56 | } 57 | 58 | /** 59 | * Create a Stripe Connect OAuth link. 60 | * 61 | * @param array|null $options 62 | * @return AuthorizeUrl 63 | * @see https://stripe.com/docs/connect/standard-accounts#integrating-oauth 64 | */ 65 | public function authorizeUrl(array $options = null) 66 | { 67 | if (!$state = $this->state->get()) { 68 | throw new RuntimeException('State parameter cannot be empty.'); 69 | } 70 | 71 | return new AuthorizeUrl($state, $options); 72 | } 73 | 74 | /** 75 | * Authorize access to an account. 76 | * 77 | * @param string $code 78 | * @param array|null $options 79 | * @return StripeObject 80 | * @see https://stripe.com/docs/connect/standard-accounts#token-request 81 | */ 82 | public function authorize($code, array $options = null) 83 | { 84 | $params = [ 85 | self::CODE => $code, 86 | self::GRANT_TYPE => self::GRANT_TYPE_AUTHORIZATION_CODE, 87 | ]; 88 | 89 | return call_user_func($this->client, OAuth::class, 'token', $params, $options); 90 | } 91 | 92 | public function refresh() 93 | { 94 | // @todo 95 | } 96 | 97 | /** 98 | * Revoke access to an account. 99 | * 100 | * @param string $accountId 101 | * @param array|null $options 102 | * @return StripeObject 103 | * @see https://stripe.com/docs/connect/standard-accounts#revoked-access 104 | */ 105 | public function deauthorize($accountId, array $options = null) 106 | { 107 | $params = [ 108 | self::STRIPE_USER_ID => $accountId, 109 | ]; 110 | 111 | return call_user_func($this->client, OAuth::class, 'deauthorize', $params, $options); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Connect/ConnectedAccount.php: -------------------------------------------------------------------------------- 1 | connect( 29 | $this->getStripeAccountIdentifier() 30 | ); 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getStripeAccountIdentifier() 37 | { 38 | return $this->{$this->getStripeAccountIdentifierName()}; 39 | } 40 | 41 | /** 42 | * Get the Stripe account ID column name. 43 | * 44 | * If your model does not use an incrementing primary key, we assume 45 | * that the primary key is also the Stripe ID. 46 | * 47 | * If your model does use incrementing primary keys, we default to 48 | * `stripe_account_id` as the column name. 49 | * 50 | * If you use a different name, just implement this method yourself. 51 | * 52 | * @return string 53 | */ 54 | public function getStripeAccountIdentifierName() 55 | { 56 | if (!$this->incrementing) { 57 | return $this->getKeyName(); 58 | } 59 | 60 | return 'stripe_account_id'; 61 | } 62 | 63 | /** 64 | * @return string|null 65 | */ 66 | public function getStripeTokenScope() 67 | { 68 | return $this->{$this->getStripeTokenScopeName()}; 69 | } 70 | 71 | /** 72 | * Get the name for the Stripe token scope. 73 | * 74 | * @return string 75 | */ 76 | public function getStripeTokenScopeName() 77 | { 78 | return $this->hasStripeKey() ? 'token_scope' : 'stripe_token_scope'; 79 | } 80 | 81 | /** 82 | * Get the Stripe refresh token. 83 | * 84 | * @return string|null 85 | */ 86 | public function getStripeRefreshToken() 87 | { 88 | return $this->{$this->getStripeRefreshTokenName()}; 89 | } 90 | 91 | /** 92 | * Get the Stripe refresh token column name. 93 | * 94 | * @return string 95 | */ 96 | public function getStripeRefreshTokenName() 97 | { 98 | return $this->hasStripeKey() ? 'refresh_token' : 'stripe_refresh_token'; 99 | } 100 | 101 | /** 102 | * Get the user id that the account is associated to. 103 | * 104 | * @return mixed|null 105 | */ 106 | public function getStripeOwnerIdentifier() 107 | { 108 | return $this->{$this->getStripeOwnerIdentifierName()}; 109 | } 110 | 111 | /** 112 | * Get the user id column name. 113 | * 114 | * If this method returns null, the user will not be stored 115 | * when an access token is fetched. 116 | * 117 | * @return string|null 118 | */ 119 | public function getStripeOwnerIdentifierName() 120 | { 121 | return 'owner_id'; 122 | } 123 | 124 | /** 125 | * Is the model using the Stripe account identifier as its key? 126 | * 127 | * @return bool 128 | */ 129 | protected function hasStripeKey() 130 | { 131 | return $this->getKeyName() === $this->getStripeAccountIdentifierName(); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Connect/Connector.php: -------------------------------------------------------------------------------- 1 | account = $account; 47 | } 48 | 49 | /** 50 | * Is the connector for the provided account? 51 | * 52 | * @param AccountInterface|string $accountId 53 | * @return bool 54 | */ 55 | public function is($accountId) 56 | { 57 | if ($accountId instanceof AccountInterface) { 58 | $accountId = $accountId->getStripeAccountIdentifier(); 59 | } 60 | 61 | return $this->id() === $accountId; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function id() 68 | { 69 | return $this->account->getStripeAccountIdentifier(); 70 | } 71 | 72 | /** 73 | * Deauthorize the connected account. 74 | * 75 | * @param iterable|array|null $options 76 | * @return void 77 | */ 78 | public function deauthorize($options = null) 79 | { 80 | app(Authorizer::class)->deauthorize( 81 | $this->accountId(), 82 | collect($options)->all() ?: null 83 | ); 84 | 85 | event(new AccountDeauthorized($this->account)); 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | protected function accountId(): string 92 | { 93 | return $this->id(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Connect/OwnsStripeAccounts.php: -------------------------------------------------------------------------------- 1 | {$this->getStripeIdentifierName()}; 37 | } 38 | 39 | /** 40 | * Get the column name of the unique identifier for the Stripe account owner. 41 | * 42 | * @return string 43 | */ 44 | public function getStripeIdentifierName(): string 45 | { 46 | if ($this instanceof Authenticatable) { 47 | return $this->getAuthIdentifierName(); 48 | } 49 | 50 | return $this->getKeyName(); 51 | } 52 | 53 | /** 54 | * @return HasMany 55 | */ 56 | public function stripeAccounts(): HasMany 57 | { 58 | $model = Config::connectModel(); 59 | 60 | if (!$owner = $model->getStripeOwnerIdentifierName()) { 61 | throw new LogicException('Stripe account model must have an owner column.'); 62 | } 63 | 64 | return $this->hasMany( 65 | get_class($model), 66 | $owner, 67 | $this->getStripeIdentifierName() 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Connect/SessionState.php: -------------------------------------------------------------------------------- 1 | session = $session; 46 | $this->request = $request; 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function get() 53 | { 54 | return $this->session->token(); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function check($value) 61 | { 62 | return $this->get() === $value; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function user() 69 | { 70 | return $this->request->user(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Connector.php: -------------------------------------------------------------------------------- 1 | accounts()->retrieve(); 56 | } 57 | 58 | /** 59 | * @return Repositories\AccountRepository 60 | */ 61 | public function accounts(): Repositories\AccountRepository 62 | { 63 | return new Repositories\AccountRepository( 64 | app(Client::class), 65 | $this->accountId() 66 | ); 67 | } 68 | 69 | /** 70 | * @return Repositories\BalanceRepository 71 | */ 72 | public function balances(): Repositories\BalanceRepository 73 | { 74 | return new Repositories\BalanceRepository( 75 | app(Client::class), 76 | $this->accountId() 77 | ); 78 | } 79 | 80 | /** 81 | * @return Repositories\ChargeRepository 82 | */ 83 | public function charges(): Repositories\ChargeRepository 84 | { 85 | return new Repositories\ChargeRepository( 86 | app(Client::class), 87 | $this->accountId() 88 | ); 89 | } 90 | 91 | /** 92 | * @return Repositories\EventRepository 93 | */ 94 | public function events(): Repositories\EventRepository 95 | { 96 | return new Repositories\EventRepository( 97 | app(Client::class), 98 | $this->accountId() 99 | ); 100 | } 101 | 102 | /** 103 | * Create a payment intents client for the provided account. 104 | * 105 | * @return Repositories\PaymentIntentRepository 106 | */ 107 | public function paymentIntents(): Repositories\PaymentIntentRepository 108 | { 109 | return new Repositories\PaymentIntentRepository( 110 | app(Client::class), 111 | $this->accountId() 112 | ); 113 | } 114 | 115 | /** 116 | * @return Repositories\RefundRepository 117 | */ 118 | public function refunds(): Repositories\RefundRepository 119 | { 120 | return new Repositories\RefundRepository( 121 | app(Client::class), 122 | $this->accountId() 123 | ); 124 | } 125 | 126 | /** 127 | * Get the account id to use when creating a repository. 128 | * 129 | * @return string|null 130 | */ 131 | protected function accountId(): ?string 132 | { 133 | return null; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/Console/Commands/StripeQuery.php: -------------------------------------------------------------------------------- 1 | argument('resource'))); 60 | $id = $this->argument('id'); 61 | $account = $this->option('account'); 62 | 63 | try { 64 | /** @var Connector $connector */ 65 | $connector = $account ? $stripe->connect($account) : $stripe->account(); 66 | 67 | if (('balances' === $resource) && $id) { 68 | throw new UnexpectedValueException('The id parameter is not supported for the balances resource.'); 69 | } 70 | 71 | /** @var AbstractRepository $repository */ 72 | $repository = call_user_func($connector, $resource); 73 | 74 | if ($expand = $this->option('expand')) { 75 | $repository->expand(...$expand); 76 | } 77 | 78 | /** Get the result */ 79 | $result = $id ? 80 | $this->retrieve($repository, $resource, $id) : 81 | $this->query($repository, $resource); 82 | } catch (UnexpectedValueException $ex) { 83 | $this->error($ex->getMessage()); 84 | return 1; 85 | } catch (ApiErrorException $ex) { 86 | $this->error('Stripe Error: ' . $ex->getMessage()); 87 | return 2; 88 | } 89 | 90 | $this->line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 91 | 92 | return 0; 93 | } 94 | 95 | /** 96 | * @param AbstractRepository $repository 97 | * @param string $resource 98 | * @param string $id 99 | * @return JsonSerializable 100 | * @throws ApiErrorException 101 | */ 102 | private function retrieve(AbstractRepository $repository, $resource, $id): JsonSerializable 103 | { 104 | if (!method_exists($repository, 'retrieve')) { 105 | throw new UnexpectedValueException("Retrieving resource '{$resource}' is not supported."); 106 | } 107 | 108 | $this->info(sprintf('Retrieving %s %s', Str::singular($resource), $id)); 109 | 110 | return $repository->retrieve($id); 111 | } 112 | 113 | /** 114 | * @param AbstractRepository $repository 115 | * @param $resource 116 | * @return JsonSerializable 117 | * @throws ApiErrorException 118 | * @todo add support for pagination. 119 | */ 120 | private function query(AbstractRepository $repository, $resource): JsonSerializable 121 | { 122 | if ('balances' === $resource) { 123 | return $repository->retrieve(); 124 | } 125 | 126 | if (!method_exists($repository, 'all')) { 127 | throw new UnexpectedValueException("Querying resource '{$resource}' is not supported."); 128 | } 129 | 130 | $this->info("Querying {$resource}"); 131 | 132 | return $repository->all(); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Contracts/Connect/AccountInterface.php: -------------------------------------------------------------------------------- 1 | owner = $owner; 67 | $this->view = $view; 68 | $this->data = $data; 69 | } 70 | 71 | /** 72 | * @param array|string $key 73 | * @param mixed|null $value 74 | * @return $this 75 | */ 76 | public function with($key, $value = null) 77 | { 78 | if (is_array($key)) { 79 | $this->data = array_merge($this->data, $key); 80 | } else { 81 | $this->data[$key] = $value; 82 | } 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Get all view data. 89 | * 90 | * @return array 91 | */ 92 | public function all() 93 | { 94 | return collect($this->data) 95 | ->merge($this->defaults()) 96 | ->put('owner', $this->owner) 97 | ->all(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Events/AccountDeauthorized.php: -------------------------------------------------------------------------------- 1 | account = $account; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Events/ClientReceivedResult.php: -------------------------------------------------------------------------------- 1 | name = $name; 57 | $this->method = $method; 58 | $this->args = $args; 59 | $this->result = $result; 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function toArray() 66 | { 67 | return [ 68 | 'name' => $this->name, 69 | 'method' => $this->method, 70 | 'args' => $this->args, 71 | 'result' => $this->result, 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Events/ClientWillSend.php: -------------------------------------------------------------------------------- 1 | name = $name; 50 | $this->method = $method; 51 | $this->args = $args; 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function toArray() 58 | { 59 | return [ 60 | 'name' => $this->name, 61 | 'method' => $this->method, 62 | 'args' => $this->args, 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Events/FetchedUserCredentials.php: -------------------------------------------------------------------------------- 1 | account = $account; 55 | $this->owner = $owner; 56 | $this->token = $token; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Events/OAuthError.php: -------------------------------------------------------------------------------- 1 | error = $code; 87 | $this->message = $description; 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | protected function defaults() 94 | { 95 | return ['error' => $this->error, 'message' => $this->message]; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Events/OAuthSuccess.php: -------------------------------------------------------------------------------- 1 | code = $code; 49 | $this->scope = $scope; 50 | } 51 | 52 | /** 53 | * Is the scope read only? 54 | * 55 | * @return bool 56 | */ 57 | public function readOnly() 58 | { 59 | return Authorizer::SCOPE_READ_ONLY === $this->scope; 60 | } 61 | 62 | /** 63 | * Is the scope read/write? 64 | * 65 | * @return bool 66 | */ 67 | public function readWrite() 68 | { 69 | return Authorizer::SCOPE_READ_WRITE === $this->scope; 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | protected function defaults() 76 | { 77 | return ['scope' => $this->scope]; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Events/SignatureVerificationFailed.php: -------------------------------------------------------------------------------- 1 | message = $message; 53 | $this->header = $header; 54 | $this->signingSecret = $signingSecret; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function signingSecret() 61 | { 62 | return Config::webhookSigningSecrect($this->signingSecret); 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function toArray() 69 | { 70 | return [ 71 | 'message' => $this->message, 72 | 'header' => $this->header, 73 | 'signing_secret' => $this->signingSecret, 74 | ]; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Exceptions/AccountNotConnectedException.php: -------------------------------------------------------------------------------- 1 | accountId = $accountId; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function accountId() 46 | { 47 | return $this->accountId; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exceptions/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | instance( 64 | Client::class, 65 | $client = new ClientFake(static::$app->make('events')) 66 | ); 67 | 68 | /** 69 | * We then swap in a Stripe service fake, that has our test assertions on it. 70 | * This extends the real Stripe service and doesn't overload anything on it, 71 | * so the service will operate exactly as expected. 72 | */ 73 | static::swap($fake = new StripeFake($client)); 74 | 75 | $fake->withQueue(...$queue); 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | protected static function getFacadeAccessor() 82 | { 83 | return 'stripe'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Http/Controllers/OAuthController.php: -------------------------------------------------------------------------------- 1 | log = $log; 45 | } 46 | 47 | /** 48 | * Handle the Stripe Connect authorize endpoint. 49 | * 50 | * @param AuthorizeConnect $request 51 | * @param StateProviderInterface $state 52 | * @return Response 53 | */ 54 | public function __invoke(AuthorizeConnect $request, StateProviderInterface $state) 55 | { 56 | $data = collect($request->query())->only([ 57 | 'code', 58 | 'scope', 59 | 'error', 60 | 'error_description', 61 | ]); 62 | 63 | $owner = $request->owner(); 64 | 65 | $this->log->log('Received OAuth redirect.', $data->all()); 66 | 67 | /** Check the state parameter and return an error if it is not as expected. */ 68 | if (true !== $state->check($request->query('state'))) { 69 | return $this->error(Response::HTTP_FORBIDDEN, [ 70 | 'error' => OAuthError::LARAVEL_STRIPE_FORBIDDEN, 71 | 'error_description' => 'Invalid authorization token.', 72 | ], $owner); 73 | } 74 | 75 | /** If Stripe has told there is an error, return an error response. */ 76 | if ($data->has('error')) { 77 | return $this->error( 78 | Response::HTTP_UNPROCESSABLE_ENTITY, 79 | $data, 80 | $owner 81 | ); 82 | } 83 | 84 | /** Otherwise return our success view. */ 85 | return $this->success($data, $owner); 86 | } 87 | 88 | /** 89 | * Handle success. 90 | * 91 | * @param $data 92 | * @param $user 93 | * @return Response 94 | */ 95 | protected function success($data, $user) 96 | { 97 | event($success = new OAuthSuccess( 98 | $data['code'], 99 | $data['scope'] ?? null, 100 | $user, 101 | Config::connectSuccessView() 102 | )); 103 | 104 | return response()->view($success->view, $success->all()); 105 | } 106 | 107 | /** 108 | * Handle an error. 109 | * 110 | * @param int $status 111 | * @param $data 112 | * @param $user 113 | * @return Response 114 | */ 115 | protected function error($status, $data, $user) 116 | { 117 | event($error = new OAuthError( 118 | $data['error'], 119 | $data['error_description'], 120 | $user, 121 | Config::connectErrorView() 122 | )); 123 | 124 | return response()->view( 125 | $error->view, 126 | $error->all(), 127 | $status 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Http/Controllers/WebhookController.php: -------------------------------------------------------------------------------- 1 | log = $log; 43 | } 44 | 45 | /** 46 | * Handle a Stripe webhook. 47 | * 48 | * @param Request $request 49 | * @param ProcessorInterface $processor 50 | * @return Response 51 | */ 52 | public function __invoke(Request $request, ProcessorInterface $processor) 53 | { 54 | if ('event' !== $request->json('object') || empty($request->json('id'))) { 55 | $this->log->log("Invalid Stripe webhook payload."); 56 | 57 | return response()->json(['error' => 'Invalid JSON payload.'], Response::HTTP_BAD_REQUEST); 58 | } 59 | 60 | $event = Event::constructFrom($request->json()->all()); 61 | 62 | /** Only process the webhook if it has not already been processed. */ 63 | if ($processor->didReceive($event)) { 64 | $this->log->log(sprintf( 65 | "Ignoring Stripe webhook %s for event %s, as it is already processed.", 66 | $event->id, 67 | $event->type 68 | )); 69 | } else { 70 | $this->log->encode("Received new Stripe webhook event {$event->type}", $event); 71 | $processor->receive($event); 72 | } 73 | 74 | return response('', Response::HTTP_NO_CONTENT); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifySignature.php: -------------------------------------------------------------------------------- 1 | verifier = $verifier; 55 | $this->events = $events; 56 | $this->log = $log; 57 | } 58 | 59 | /** 60 | * @param $request 61 | * @param \Closure $next 62 | * @param string $signingSecret 63 | * @return mixed 64 | */ 65 | public function handle($request, \Closure $next, $signingSecret = 'default') 66 | { 67 | $this->log->log("Verifying Stripe webhook using signing secret: {$signingSecret}"); 68 | 69 | try { 70 | $this->verifier->verify($request, $signingSecret); 71 | } catch (SignatureVerificationException $ex) { 72 | $event = new SignatureVerificationFailed( 73 | $ex->getMessage(), 74 | $ex->getSigHeader(), 75 | $signingSecret 76 | ); 77 | 78 | $this->log->log("Stripe webhook signature verification failed.", $event->toArray()); 79 | $this->events->dispatch($event); 80 | 81 | return response()->json(['error' => 'Invalid signature.'], Response::HTTP_BAD_REQUEST); 82 | } 83 | 84 | $this->log->log("Verified Stripe webhook with signing secret: {$signingSecret}"); 85 | 86 | return $next($request); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Http/Requests/AuthorizeConnect.php: -------------------------------------------------------------------------------- 1 | [ 39 | 'required_without:error', 40 | 'string', 41 | ], 42 | 'state' => [ 43 | 'required', 44 | 'string', 45 | ], 46 | 'scope' => [ 47 | 'sometimes', 48 | Rule::in(AuthorizeUrl::scopes()), 49 | ], 50 | 'error' => [ 51 | 'required_without:code', 52 | 'string', 53 | ], 54 | 'error_description' => [ 55 | 'required_with:error', 56 | 'string', 57 | ], 58 | ]; 59 | } 60 | 61 | /** 62 | * Authorize the request. 63 | * 64 | * @return bool 65 | */ 66 | public function authorize() 67 | { 68 | return $this->owner() instanceof AccountOwnerInterface; 69 | } 70 | 71 | /** 72 | * Get the Stripe account owner for the request. 73 | * 74 | * @return AccountOwnerInterface 75 | */ 76 | public function owner() 77 | { 78 | if ($fn = LaravelStripe::$currentOwnerResolver) { 79 | return call_user_func($fn, $this); 80 | } 81 | 82 | return $this->user(); 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function validationData() 89 | { 90 | return $this->query(); 91 | } 92 | 93 | /** 94 | * Handle validation failing. 95 | * 96 | * We do not expect this scenario to occur, because Stripe has defined 97 | * the parameter it sends us. However we handle the scenario just in case. 98 | * 99 | * We do not throw the Laravel validation exception, because by default 100 | * Laravel turns this into a redirect response to send the user back... 101 | * but this does not make sense in our scenario. 102 | * 103 | * @param Validator $validator 104 | * @throws HttpException 105 | */ 106 | protected function failedValidation(Validator $validator) 107 | { 108 | throw new HttpException(Response::HTTP_BAD_REQUEST); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Jobs/FetchUserCredentials.php: -------------------------------------------------------------------------------- 1 | code = $code; 60 | $this->scope = $scope; 61 | $this->owner = $owner; 62 | } 63 | 64 | /** 65 | * Execute the job. 66 | * 67 | * @param Authorizer $authorizer 68 | * @param AdapterInterface $adapter 69 | * @return void 70 | */ 71 | public function handle(Authorizer $authorizer, AdapterInterface $adapter) 72 | { 73 | $token = $authorizer->authorize($this->code); 74 | 75 | $account = $adapter->store( 76 | $token['stripe_user_id'], 77 | $token['refresh_token'], 78 | $token['scope'], 79 | $this->owner 80 | ); 81 | 82 | event(new FetchedUserCredentials($account, $this->owner, $token)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Jobs/ProcessWebhook.php: -------------------------------------------------------------------------------- 1 | event = $event; 56 | $this->payload = $payload; 57 | } 58 | 59 | /** 60 | * Execute the job. 61 | * 62 | * @param ProcessorInterface $processor 63 | * @param Logger $log 64 | * @return void 65 | * @throws \Throwable 66 | */ 67 | public function handle(ProcessorInterface $processor, Logger $log) 68 | { 69 | $webhook = Event::constructFrom($this->payload); 70 | 71 | $log->log( 72 | "Processing webhook {$webhook->id}.", 73 | collect($this->payload)->only('account', 'type')->all() 74 | ); 75 | 76 | $this->event->getConnection()->transaction(function () use ($processor, $webhook) { 77 | $processor->dispatch($webhook, $this->event); 78 | }); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/LaravelStripe.php: -------------------------------------------------------------------------------- 1 | code, 39 | $event->scope, 40 | $event->owner 41 | ); 42 | 43 | $job->onQueue($config['queue'])->onConnection($config['connection']); 44 | 45 | dispatch($job); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Listeners/DispatchWebhookJob.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 47 | $this->log = $log; 48 | } 49 | 50 | /** 51 | * Handle the event. 52 | * 53 | * @param Webhook $webhook 54 | * @return void 55 | */ 56 | public function handle(Webhook $webhook) 57 | { 58 | if (!$job = $webhook->job()) { 59 | return; 60 | } 61 | 62 | /** @var Queueable $job */ 63 | $job = new $job($webhook); 64 | $job->onConnection($webhook->connection()); 65 | $job->onQueue($webhook->queue()); 66 | 67 | $this->log->log("Dispatching job for webhook '{$webhook->type()}'.", [ 68 | 'id' => $webhook->id(), 69 | 'connection' => $webhook->connection(), 70 | 'queue' => $webhook->queue(), 71 | 'job' => $webhook->job(), 72 | ]); 73 | 74 | $this->queue->dispatch($job); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Listeners/LogClientRequests.php: -------------------------------------------------------------------------------- 1 | log = $log; 39 | } 40 | 41 | /** 42 | * Handle the event. 43 | * 44 | * @param ClientWillSend $event 45 | * @return void 46 | */ 47 | public function handle(ClientWillSend $event) 48 | { 49 | $this->log->log( 50 | "Sending {$event->name}.{$event->method}", 51 | $event->toArray() 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Listeners/LogClientResults.php: -------------------------------------------------------------------------------- 1 | log = $log; 37 | } 38 | 39 | /** 40 | * Handle the event. 41 | * 42 | * @param ClientReceivedResult $event 43 | * @return void 44 | */ 45 | public function handle(ClientReceivedResult $event) 46 | { 47 | $message = "Result for {$event->name}.{$event->method}"; 48 | $context = $event->toArray(); 49 | 50 | if (!$event->result instanceof JsonSerializable) { 51 | $this->log->log($message, $context); 52 | return; 53 | } 54 | 55 | unset($context['result']); 56 | $this->log->encode($message, $event->result, $context); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Listeners/RemoveAccountOnDeauthorize.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 39 | } 40 | 41 | /** 42 | * Handle the event. 43 | * 44 | * @param AccountDeauthorized $event 45 | * @return void 46 | */ 47 | public function handle(AccountDeauthorized $event) 48 | { 49 | $this->adapter->remove($event->account); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Log/Logger.php: -------------------------------------------------------------------------------- 1 | log = $log; 54 | $this->level = $level ?: 'debug'; 55 | $this->exclude = $exclude; 56 | } 57 | 58 | /** 59 | * Log a message at the configured level. 60 | * 61 | * @param string $message 62 | * @param array $context 63 | * @return void 64 | */ 65 | public function log($message, array $context = []) 66 | { 67 | $this->log->log($this->level, 'Stripe: ' . $message, $context); 68 | } 69 | 70 | /** 71 | * Encode data into an error message. 72 | * 73 | * @param string $message 74 | * @param mixed $data 75 | * @param array $context 76 | * @return void 77 | */ 78 | public function encode($message, $data, array $context = []) 79 | { 80 | $message .= ':' . PHP_EOL . $this->toJson($data); 81 | 82 | $this->log($message, $context); 83 | } 84 | 85 | /** 86 | * Encode a Stripe object for a log message. 87 | * 88 | * @param mixed $data 89 | * @return string 90 | */ 91 | private function toJson($data) 92 | { 93 | if ($data instanceof JsonSerializable) { 94 | $data = $data->jsonSerialize(); 95 | } 96 | 97 | $data = $this->serialize((array) $data); 98 | 99 | return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 100 | } 101 | 102 | /** 103 | * @param array $data 104 | * @return array 105 | */ 106 | private function serialize(array $data) 107 | { 108 | $this->sanitise($data); 109 | 110 | return collect($data)->map(function ($value) { 111 | return is_array($value) ? $this->serialize($value) : $value; 112 | })->all(); 113 | } 114 | 115 | /** 116 | * @param array $data 117 | */ 118 | private function sanitise(array &$data) 119 | { 120 | $name = isset($data['object']) ? $data['object'] : null; 121 | 122 | /** Stripe webhooks contain an object key that is not a string. */ 123 | if (!is_string($name)) { 124 | return; 125 | } 126 | 127 | foreach ($this->exclude($data['object']) as $path) { 128 | if (!$value = Arr::get($data, $path)) { 129 | continue; 130 | } 131 | 132 | if (is_string($value)) { 133 | Arr::set($data, $path, '***'); 134 | } else { 135 | Arr::forget($data, $path); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Get the paths to exclude from logging. 142 | * 143 | * @param mixed $name 144 | * @return array 145 | */ 146 | private function exclude($name) 147 | { 148 | if (isset($this->exclude[$name])) { 149 | return (array) $this->exclude[$name]; 150 | } 151 | 152 | return $this->exclude[$name] = []; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Models/StripeAccount.php: -------------------------------------------------------------------------------- 1 | 'json', 66 | 'capabilities' => 'json', 67 | 'charges_enabled' => 'boolean', 68 | 'deleted_at' => 'datetime', 69 | 'details_submitted' => 'boolean', 70 | 'individual' => 'json', 71 | 'metadata' => 'json', 72 | 'payouts_enabled' => 'boolean', 73 | 'requirements' => 'json', 74 | 'settings' => 'json', 75 | 'tos_acceptance' => 'json', 76 | ]; 77 | 78 | /** 79 | * @return HasMany 80 | */ 81 | public function events() 82 | { 83 | $model = Config::webhookModel(); 84 | 85 | return $this->hasMany( 86 | get_class($model), 87 | $model->getAccountIdentifierName(), 88 | $this->getStripeAccountIdentifierName() 89 | ); 90 | } 91 | 92 | /** 93 | * @return BelongsTo 94 | */ 95 | public function owner() 96 | { 97 | $model = Config::connectOwner(); 98 | 99 | return $this->belongsTo( 100 | get_class($model), 101 | $this->getStripeOwnerIdentifierName(), 102 | $model->getStripeIdentifierName(), 103 | 'owner' 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Models/StripeEvent.php: -------------------------------------------------------------------------------- 1 | 'datetime', 53 | 'livemode' => 'boolean', 54 | 'pending_webhooks' => 'integer', 55 | 'request' => 'json', 56 | ]; 57 | 58 | /** 59 | * @return BelongsTo 60 | */ 61 | public function account() 62 | { 63 | $model = Config::connectModel(); 64 | 65 | return new BelongsTo( 66 | $model->newQuery(), 67 | $this, 68 | $this->getAccountIdentifierName(), 69 | $model->getStripeAccountIdentifierName(), 70 | 'account' 71 | ); 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getAccountIdentifierName() 78 | { 79 | return 'account_id'; 80 | } 81 | 82 | /** 83 | * Get the Stripe connector for the account that this belongs to. 84 | * 85 | * @return Connector 86 | * @throws AccountNotConnectedException 87 | */ 88 | public function stripe() 89 | { 90 | if ($account = $this->account_id) { 91 | return app('stripe')->connect($account); 92 | } 93 | 94 | return app('stripe')->account(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Repositories/AccountRepository.php: -------------------------------------------------------------------------------- 1 | params($params)->param('type', $type); 44 | 45 | return $this->send('create', $this->params ?: null, $this->options ?: null); 46 | } 47 | 48 | /** 49 | * Retrieve a Stripe account. 50 | * 51 | * If the id is not provided, the account associated with this 52 | * repository is returned. 53 | * 54 | * @param string|null $id 55 | * @return Account 56 | */ 57 | public function retrieve(string $id = null): Account 58 | { 59 | if (!is_string($id) && !is_null($id)) { 60 | throw new InvalidArgumentException('Expecting a string or null.'); 61 | } 62 | 63 | if ($id) { 64 | $this->param('id', $id); 65 | } 66 | 67 | return $this->send('retrieve', $this->params ?: null, $this->options ?: null); 68 | } 69 | 70 | /** 71 | * @inheritDoc 72 | */ 73 | protected function fqn(): string 74 | { 75 | return Account::class; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/Repositories/BalanceRepository.php: -------------------------------------------------------------------------------- 1 | send('retrieve', $this->options ?: null); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | protected function fqn(): string 42 | { 43 | return Balance::class; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Repositories/ChargeRepository.php: -------------------------------------------------------------------------------- 1 | params($params)->params( 48 | compact('currency', 'amount') 49 | ); 50 | 51 | return $this->send( 52 | 'create', 53 | $this->params ?: null, 54 | $this->options ?: null 55 | ); 56 | } 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | protected function fqn(): string 62 | { 63 | return Charge::class; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Repositories/Concerns/All.php: -------------------------------------------------------------------------------- 1 | params($params); 42 | 43 | return $this->send('all', $this->params ?: null, $this->options ?: null); 44 | } 45 | 46 | /** 47 | * Query all resources, and return a Laravel collection. 48 | * 49 | * @param iterable|array $params 50 | * @return IlluminateCollection 51 | */ 52 | public function collect($params = []): IlluminateCollection 53 | { 54 | return collect($this->all($params)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Repositories/Concerns/HasMetadata.php: -------------------------------------------------------------------------------- 1 | param( 36 | AbstractRepository::PARAM_METADATA, 37 | collect($meta)->toArray() 38 | ); 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Repositories/Concerns/Retrieve.php: -------------------------------------------------------------------------------- 1 | param(self::PARAM_ID, $id); 41 | 42 | return $this->send('retrieve', $this->params, $this->options ?: null); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Repositories/Concerns/Update.php: -------------------------------------------------------------------------------- 1 | params($params); 39 | 40 | return $this->send( 41 | 'update', 42 | $id, 43 | $this->params ?: null, 44 | $this->options ?: null 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Repositories/EventRepository.php: -------------------------------------------------------------------------------- 1 | params($params)->params( 48 | compact('currency', 'amount') 49 | ); 50 | 51 | return $this->send('create', $this->params, $this->options); 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | protected function fqn(): string 58 | { 59 | return PaymentIntent::class; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Repositories/RefundRepository.php: -------------------------------------------------------------------------------- 1 | create($charge, $params); 49 | } 50 | 51 | /** 52 | * Create a partial refund. 53 | * 54 | * @param Charge|string $charge 55 | * @param int $amount 56 | * @param iterable|array $params 57 | * @return Refund 58 | */ 59 | public function partial($charge, int $amount, iterable $params = []): Refund 60 | { 61 | Assert::zeroDecimal($amount); 62 | 63 | $params['amount'] = $amount; 64 | 65 | return $this->create($charge, $params); 66 | } 67 | 68 | /** 69 | * Create a refund. 70 | * 71 | * @param Charge|string $charge 72 | * @param iterable|array $params 73 | * @return Refund 74 | */ 75 | public function create($charge, iterable $params = []): Refund 76 | { 77 | if ($charge instanceof Charge) { 78 | $charge = $charge->id; 79 | } 80 | 81 | $this->params($params)->param('charge', $charge); 82 | 83 | return $this->send( 84 | 'create', 85 | $this->params ?: null, 86 | $this->options ?: null 87 | ); 88 | } 89 | 90 | /** 91 | * Update a refund. 92 | * 93 | * This request only accepts the `metadata` as an argument. 94 | * 95 | * @param string $id 96 | * @param Collection|iterable|array $metadata 97 | * @return Refund 98 | */ 99 | public function update(string $id, iterable $metadata): Refund 100 | { 101 | $this->metadata($metadata); 102 | 103 | return $this->send( 104 | 'update', 105 | $id, 106 | $this->params ?: null, 107 | $this->options ?: null 108 | ); 109 | } 110 | 111 | /** 112 | * @inheritDoc 113 | */ 114 | protected function fqn(): string 115 | { 116 | return Refund::class; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/StripeService.php: -------------------------------------------------------------------------------- 1 | middleware( 42 | "stripe.verify:{$signingSecret}" 43 | ); 44 | } 45 | 46 | /** 47 | * Register an Connect OAuth endpoint. 48 | * 49 | * @param $uri 50 | * @return \Illuminate\Routing\Route 51 | */ 52 | public function oauth($uri) 53 | { 54 | return Route::get($uri, '\\' . OAuthController::class); 55 | } 56 | 57 | /** 58 | * Access the main application account. 59 | * 60 | * @return Connector 61 | */ 62 | public function account() 63 | { 64 | return new Connector(); 65 | } 66 | 67 | /** 68 | * Access a connected account. 69 | * 70 | * @param AccountInterface|string $accountId 71 | * @return Connect\Connector 72 | * @throws AccountNotConnectedException 73 | */ 74 | public function connect($accountId) 75 | { 76 | if ($accountId instanceof AccountInterface) { 77 | return new Connect\Connector($accountId); 78 | } 79 | 80 | if ($account = $this->connectAccount($accountId)) { 81 | return new Connect\Connector($account); 82 | } 83 | 84 | throw new AccountNotConnectedException($accountId); 85 | } 86 | 87 | /** 88 | * Get a Stripe Connect account by id. 89 | * 90 | * @param $accountId 91 | * @return AccountInterface|null 92 | */ 93 | public function connectAccount($accountId) 94 | { 95 | return app('stripe.connect')->find($accountId); 96 | } 97 | 98 | /** 99 | * Create a Stripe Connect OAuth link. 100 | * 101 | * @param array|null $options 102 | * @return AuthorizeUrl 103 | */ 104 | public function authorizeUrl(array $options = null) 105 | { 106 | return app(Authorizer::class)->authorizeUrl($options); 107 | } 108 | 109 | /** 110 | * Log a Stripe object, sanitising any sensitive data. 111 | * 112 | * @param string $message 113 | * @param mixed $data 114 | * @param array $context 115 | */ 116 | public function log($message, $data, array $context = []) 117 | { 118 | app('stripe.log')->encode($message, $data, $context); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Testing/ClientFake.php: -------------------------------------------------------------------------------- 1 | queue = collect(); 53 | $this->history = collect(); 54 | $this->counter = 0; 55 | } 56 | 57 | /** 58 | * Queue results. 59 | * 60 | * @param StripeObject ...$results 61 | * @return void 62 | */ 63 | public function queue(StripeObject ...$results) 64 | { 65 | $this->queue = $this->queue->merge($results); 66 | } 67 | 68 | /** 69 | * Get the call history index. 70 | * 71 | * @return int 72 | */ 73 | public function index() 74 | { 75 | return $this->counter; 76 | } 77 | 78 | /** 79 | * Get the current index, then increment it. 80 | * 81 | * @return int 82 | */ 83 | public function increment() 84 | { 85 | $index = $this->index(); 86 | 87 | ++$this->counter; 88 | 89 | return $index; 90 | } 91 | 92 | /** 93 | * @param int $index 94 | * @return array|null 95 | */ 96 | public function at($index) 97 | { 98 | return $this->history->get($index); 99 | } 100 | 101 | /** 102 | * @param $class 103 | * @param $method 104 | * @param array $args 105 | * @return StripeObject 106 | */ 107 | protected function execute($class, $method, array $args) 108 | { 109 | if ($this->queue->isEmpty()) { 110 | Assert::fail(("Unexpected Stripe call: {$class}::{$method}")); 111 | } 112 | 113 | $this->history->push([ 114 | 'class' => $class, 115 | 'method' => $method, 116 | 'args' => $args, 117 | 'result' => $result = $this->queue->shift() 118 | ]); 119 | 120 | return $result; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Testing/Concerns/MakesStripeAssertions.php: -------------------------------------------------------------------------------- 1 | stripeClient->queue(...$objects); 42 | } 43 | 44 | /** 45 | * Assert the next Stripe call in the history. 46 | * 47 | * @param $class 48 | * the expected fully qualified class name. 49 | * @param $method 50 | * the expected static method. 51 | * @param Closure|null $args 52 | * an optional closure to assert that the call received the correct arguments. 53 | */ 54 | public function assertInvoked($class, $method, Closure $args = null) 55 | { 56 | $index = $this->stripeClient->increment(); 57 | 58 | $this->assertInvokedAt($index, $class, $method, $args); 59 | } 60 | 61 | /** 62 | * Assert the next Stripe call in the history. 63 | * 64 | * @param int $index 65 | * the index in the history of Stripe calls. 66 | * @param $class 67 | * the expected fully qualified class name. 68 | * @param $method 69 | * the expected static method. 70 | * @param Closure|null $args 71 | * an optional closure to assert that the call received the correct arguments. 72 | */ 73 | public function assertInvokedAt($index, $class, $method, Closure $args = null) 74 | { 75 | if (!$history = $this->stripeClient->at($index)) { 76 | Assert::fail("No Stripe call at index {$index}."); 77 | } 78 | 79 | Assert::assertSame( 80 | $class . '::' . $method, 81 | $history['class'] . '::' . $history['method'], 82 | "Stripe {$index}: class and method" 83 | ); 84 | 85 | if ($args) { 86 | Assert::assertTrue( 87 | $args(...$history['args']), 88 | "Stripe {$index}: arguments" 89 | ); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/Testing/StripeFake.php: -------------------------------------------------------------------------------- 1 | stripeClient = $client; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Webhooks/ConnectWebhook.php: -------------------------------------------------------------------------------- 1 | account = $account; 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function connect() 67 | { 68 | return true; 69 | } 70 | 71 | /** 72 | * @return string|null 73 | */ 74 | public function account() 75 | { 76 | return $this->webhook['account']; 77 | } 78 | 79 | /** 80 | * Is the webhook for the supplied account? 81 | * 82 | * @param AccountInterface|string $account 83 | * @return bool 84 | */ 85 | public function accountIs($account) 86 | { 87 | if ($account instanceof AccountInterface) { 88 | $account = $account->getStripeAccountId(); 89 | } 90 | 91 | return $this->account() === $account; 92 | } 93 | 94 | /** 95 | * Is the webhook not for the specified account? 96 | * 97 | * @param AccountInterface|string $account 98 | * @return bool 99 | */ 100 | public function accountIsNot($account) 101 | { 102 | return !$this->accountIs($account); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Webhooks/Verifier.php: -------------------------------------------------------------------------------- 1 | header(self::SIGNATURE_HEADER)) { 42 | throw SignatureVerificationException::factory( 43 | 'Expecting ' . self::SIGNATURE_HEADER . ' header.', 44 | $request->getContent(), 45 | $header 46 | ); 47 | } 48 | 49 | WebhookSignature::verifyHeader( 50 | $request->getContent(), 51 | $header, 52 | Config::webhookSigningSecrect($name), 53 | Config::webhookTolerance() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Webhooks/Webhook.php: -------------------------------------------------------------------------------- 1 | webhook = $webhook; 59 | $this->model = $model; 60 | $this->config = $config; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function id() 67 | { 68 | return $this->webhook->id; 69 | } 70 | 71 | /** 72 | * Get the type of webhook. 73 | * 74 | * @return string 75 | */ 76 | public function type() 77 | { 78 | return $this->webhook->type; 79 | } 80 | 81 | /** 82 | * Is this a Connect webhook? 83 | * 84 | * Useful for listeners or jobs that run on both account and Connect webhooks. 85 | * 86 | * @return bool 87 | */ 88 | public function connect() 89 | { 90 | return false; 91 | } 92 | 93 | /** 94 | * Is the webhook the specified type? 95 | * 96 | * @param $type 97 | * @return bool 98 | */ 99 | public function is($type) 100 | { 101 | return $this->type() === $type; 102 | } 103 | 104 | /** 105 | * Is the webhook not the specified type. 106 | * 107 | * @param string $type 108 | * @return bool 109 | */ 110 | public function isNot($type) 111 | { 112 | return !$this->is($type); 113 | } 114 | 115 | /** 116 | * Get the configured queue for the webhook. 117 | * 118 | * @return string|null 119 | */ 120 | public function queue() 121 | { 122 | return Arr::get($this->config, 'queue'); 123 | } 124 | 125 | /** 126 | * Get the configured connection for the webhook. 127 | * 128 | * @return 129 | */ 130 | public function connection() 131 | { 132 | return Arr::get($this->config, 'connection'); 133 | } 134 | 135 | /** 136 | * Get the configured job for the webhook. 137 | * 138 | * @return string|null 139 | */ 140 | public function job() 141 | { 142 | return Arr::get($this->config, 'job'); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /tests/database/factories/TestFactory.php: -------------------------------------------------------------------------------- 1 | define(TestAccount::class, function (Faker $faker) { 27 | return [ 28 | 'id' => $faker->unique()->lexify('acct_????????????'), 29 | 'name' => $faker->company(), 30 | ]; 31 | }); 32 | 33 | $factory->define(TestUser::class, function (Faker $faker) { 34 | static $password; 35 | 36 | return [ 37 | 'name' => $faker->name(), 38 | 'email' => $faker->unique()->safeEmail(), 39 | 'password' => $password ?: $password = bcrypt('secret'), 40 | 'remember_token' => Str::random(10), 41 | ]; 42 | }); 43 | -------------------------------------------------------------------------------- /tests/database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 33 | $table->string('name'); 34 | $table->string('email')->unique(); 35 | $table->string('password'); 36 | $table->rememberToken(); 37 | $table->timestamps(); 38 | }); 39 | } 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | Schema::dropIfExists('users'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/database/migrations/2019_07_16_000000_create_test_tables.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 34 | $table->timestamps(); 35 | $table->string('name'); 36 | }); 37 | } 38 | 39 | /** 40 | * Reverse the migration. 41 | * 42 | * @return void 43 | */ 44 | public function down() 45 | { 46 | Schema::dropIfExists('test_accounts'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/lib/Integration/Connect/DeauthorizeTest.php: -------------------------------------------------------------------------------- 1 | create(); 40 | 41 | $account->stripe()->deauthorize(['foo' => 'bar']); 42 | 43 | Stripe::assertInvoked(OAuth::class, 'deauthorize', function ($params, $options) use ($account) { 44 | $this->assertSame(['stripe_user_id' => $account->id], $params, 'params'); 45 | $this->assertSame(['foo' => 'bar'], $options, 'options'); 46 | return true; 47 | }); 48 | 49 | Event::assertDispatched(AccountDeauthorized::class, function ($event) use ($account) { 50 | $this->assertTrue($account->is($event->account), 'event account'); 51 | return true; 52 | }); 53 | } 54 | 55 | public function testDeletesOnEvent() 56 | { 57 | Stripe::fake(new StripeObject()); 58 | 59 | $account = factory(StripeAccount::class)->create([ 60 | 'refresh_token' => 'access_token', 61 | 'token_scope' => 'read_write', 62 | ]); 63 | 64 | Stripe::connect($account)->deauthorize(['foo' => 'bar']); 65 | 66 | Stripe::assertInvoked(OAuth::class, 'deauthorize', function ($params, $options) use ($account) { 67 | $this->assertSame(['stripe_user_id' => $account->id], $params, 'params'); 68 | $this->assertSame(['foo' => 'bar'], $options, 'options'); 69 | return true; 70 | }); 71 | 72 | $this->assertDatabaseHas('stripe_accounts', [ 73 | $account->getKeyName() => $account->getKey(), 74 | 'deleted_at' => Carbon::now()->toDateTimeString(), 75 | 'refresh_token' => null, 76 | 'token_scope' => null, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/lib/Integration/EloquentTest.php: -------------------------------------------------------------------------------- 1 | set('stripe.connect.model', TestAccount::class); 33 | } 34 | 35 | public function test() 36 | { 37 | /** @var TestAccount $model */ 38 | $model = factory(TestAccount::class)->create(); 39 | 40 | $this->assertSame($model->getKeyName(), $model->getStripeAccountIdentifierName(), 'key name'); 41 | $this->assertSame($model->getKey(), $model->getStripeAccountIdentifier(), 'key'); 42 | $this->assertTrue($model->stripe()->is($model), 'model connector'); 43 | $this->assertTrue(Stripe::connect($model->id)->is($model), 'facade account connector'); 44 | } 45 | 46 | public function testIncrementing() 47 | { 48 | /** @var TestAccount $model */ 49 | $model = factory(TestAccount::class)->make(); 50 | $model->incrementing = true; 51 | 52 | $this->assertSame('stripe_account_id', $model->getStripeAccountIdentifierName()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/lib/Integration/TestCase.php: -------------------------------------------------------------------------------- 1 | app['migrator']->path(__DIR__ . '/../../database/migrations'); 44 | $this->app->make(ModelFactory::class)->load(__DIR__ . '/../../database/factories'); 45 | 46 | if (method_exists($this, 'withoutMockingConsoleOutput')) { 47 | $this->withoutMockingConsoleOutput(); 48 | } 49 | 50 | $this->app['view']->addNamespace('test', __DIR__ . '/../../resources/views'); 51 | 52 | $this->artisan('migrate', ['--database' => 'testbench']); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | protected function tearDown(): void 59 | { 60 | parent::tearDown(); 61 | Carbon::setTestNow(); 62 | } 63 | 64 | /** 65 | * Provider for all Stripe classes that are implemented via repositories. 66 | * 67 | * Balances are omitted because they do not have an id. 68 | * 69 | * @return array 70 | */ 71 | public function classProvider(): array 72 | { 73 | return [ 74 | 'accounts' => [\Stripe\Account::class, 'accounts'], 75 | 'charges' => [\Stripe\Charge::class, 'charges'], 76 | 'events' => [\Stripe\Event::class, 'events'], 77 | 'payment_intents' => [\Stripe\PaymentIntent::class, 'payment_intents'], 78 | ]; 79 | } 80 | 81 | /** 82 | * Get package providers. 83 | * 84 | * To ensure this package works with Cashier, we also include 85 | * Cashier. 86 | * 87 | * @param Application $app 88 | * @return array 89 | */ 90 | protected function getPackageProviders($app) 91 | { 92 | return [ 93 | CashierServiceProvider::class, 94 | ServiceProvider::class, 95 | ]; 96 | } 97 | 98 | /** 99 | * Get facade aliases. 100 | * 101 | * @param Application $app 102 | * @return array 103 | */ 104 | protected function getPackageAliases($app) 105 | { 106 | return [ 107 | 'Stripe' => Stripe::class, 108 | ]; 109 | } 110 | 111 | /** 112 | * Setup the test environment. 113 | * 114 | * @param Application $app 115 | * @return void 116 | */ 117 | protected function getEnvironmentSetUp($app) 118 | { 119 | /** Include our default config. */ 120 | $app['config']->set('stripe', require __DIR__ . '/../../../config/stripe.php'); 121 | 122 | /** Override views to use our test namespace */ 123 | $app['config']->set('stripe.connect.views', [ 124 | 'success' => 'test::oauth.success', 125 | 'error' => 'test::oauth.error', 126 | ]); 127 | 128 | /** Setup a test database. */ 129 | $app['config']->set('database.default', 'testbench'); 130 | $app['config']->set('database.connections.testbench', [ 131 | 'driver' => 'sqlite', 132 | 'database' => ':memory:', 133 | 'prefix' => '', 134 | ]); 135 | } 136 | 137 | /** 138 | * Load a stub. 139 | * 140 | * @param string $name 141 | * @return array 142 | */ 143 | protected function stub($name) 144 | { 145 | return json_decode( 146 | file_get_contents(__DIR__ . '/../../stubs/' . $name . '.json'), 147 | true 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/lib/Integration/Webhooks/ListenersTest.php: -------------------------------------------------------------------------------- 1 | 'evt_00000000', 49 | 'type' => 'payment_intent.succeeded', 50 | ]); 51 | 52 | event('stripe.webhooks', $webhook = new Webhook( 53 | $event, 54 | factory(StripeEvent::class)->create(), 55 | ['queue' => 'my_queue', 'connection' => 'my_connection', 'job' => TestWebhookJob::class] 56 | )); 57 | 58 | Queue::assertPushedOn('my_queue', TestWebhookJob::class, function ($job) use ($webhook) { 59 | $this->assertSame($webhook, $job->webhook); 60 | $this->assertSame('my_queue', $job->queue, 'queue'); 61 | $this->assertSame('my_connection', $job->connection, 'connection'); 62 | return true; 63 | }); 64 | } 65 | 66 | /** 67 | * If there are any configured webhook jobs, we expect them to be dispatched 68 | * when the `stripe.connect.webhooks` event is fired. They should be dispatched on the 69 | * same queue and connection as the webhook itself. 70 | */ 71 | public function testConnect() 72 | { 73 | $event = Event::constructFrom([ 74 | 'id' => 'evt_00000000', 75 | 'type' => 'payment_intent.succeeded', 76 | 'account' => 'acct_0000000000', 77 | ]); 78 | 79 | $model = factory(StripeEvent::class)->states('connect')->create(); 80 | 81 | event('stripe.connect.webhooks', $webhook = new ConnectWebhook( 82 | $event, 83 | $model->account, 84 | $model, 85 | ['job' => TestWebhookJob::class] 86 | )); 87 | 88 | Queue::assertPushed(TestWebhookJob::class, function ($job) use ($webhook) { 89 | $this->assertSame($webhook, $job->webhook); 90 | $this->assertNull($job->queue, 'queue'); 91 | $this->assertNull($job->connection, 'connection'); 92 | return true; 93 | }); 94 | } 95 | 96 | /** 97 | * Test it does not dispatch a job if the webhook is not for the specified event type. 98 | */ 99 | public function testDoesNotDispatch() 100 | { 101 | event('stripe.webhooks', $webhook = new Webhook( 102 | Event::constructFrom(['id' => 'evt_00000000', 'type' => 'charge.refunded']), 103 | factory(StripeEvent::class)->create(), 104 | ['job' => null] 105 | )); 106 | 107 | Queue::assertNotPushed(TestWebhookJob::class); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/lib/Integration/Webhooks/ProcessTest.php: -------------------------------------------------------------------------------- 1 | create([ 43 | 'updated_at' => Carbon::now()->subMinute(), 44 | ]); 45 | 46 | $payload = [ 47 | 'id' => $model->getKey(), 48 | 'type' => 'charge.failed', 49 | ]; 50 | 51 | dispatch(new ProcessWebhook($model, $payload)); 52 | 53 | $expected = [ 54 | 'stripe.webhooks', 55 | 'stripe.webhooks:charge', 56 | 'stripe.webhooks:charge.failed', 57 | ]; 58 | 59 | foreach ($expected as $name) { 60 | Event::assertDispatched( 61 | $name, 62 | function ($ev, Webhook $webhook) use ($name, $model, $payload) { 63 | $this->assertNotInstanceOf(ConnectWebhook::class, $webhook, 'not connect'); 64 | $this->assertEquals(\Stripe\Event::constructFrom($payload), $webhook->webhook, "{$name}: webhook"); 65 | $this->assertTrue($model->is($webhook->model), "{$name}: model"); 66 | return true; 67 | } 68 | ); 69 | } 70 | 71 | /** Ensure the model had its timestamp updated. */ 72 | $this->assertDatabaseHas('stripe_events', [ 73 | $model->getKeyName() => $model->getKey(), 74 | 'updated_at' => Carbon::now()->toDateTimeString(), 75 | ]); 76 | } 77 | 78 | public function testConnect() 79 | { 80 | $model = factory(StripeEvent::class)->states('connect')->create(); 81 | 82 | $payload = [ 83 | 'id' => $model->getKey(), 84 | 'account' => $model->account_id, 85 | 'type' => 'payment_intent.succeeded', 86 | ]; 87 | 88 | $job = new ProcessWebhook($model, $payload); 89 | $job->onConnection('sync')->onQueue('my_queue'); 90 | 91 | dispatch($job); 92 | 93 | $expected = [ 94 | 'stripe.connect.webhooks', 95 | 'stripe.connect.webhooks:payment_intent', 96 | 'stripe.connect.webhooks:payment_intent.succeeded', 97 | ]; 98 | 99 | foreach ($expected as $name) { 100 | Event::assertDispatched( 101 | $name, 102 | function ($ev, ConnectWebhook $webhook) use ($name, $model, $payload) { 103 | $this->assertEquals(\Stripe\Event::constructFrom($payload), $webhook->webhook, "{$name}: webhook"); 104 | $this->assertTrue($model->account->is($webhook->account), "{$name}: account"); 105 | $this->assertTrue($model->is($webhook->model), "{$name}: model"); 106 | return true; 107 | } 108 | ); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/lib/Integration/Webhooks/WebhookTest.php: -------------------------------------------------------------------------------- 1 | stub('webhook')); 31 | $model = factory(StripeEvent::class)->create(); 32 | 33 | $event = new Webhook($webhook, $model); 34 | 35 | $serialized = unserialize(serialize($event)); 36 | 37 | $this->assertEquals($event->webhook, $webhook, 'same webhook'); 38 | $this->assertTrue($model->is($serialized->model), 'same model'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/lib/TestAccount.php: -------------------------------------------------------------------------------- 1 | webhook = $webhook; 45 | } 46 | 47 | /** 48 | * Execute the job. 49 | * 50 | * @return void 51 | */ 52 | public function handle() 53 | { 54 | // noop 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/lib/Unit/Connect/AuthorizeUrlTest.php: -------------------------------------------------------------------------------- 1 | url = new AuthorizeUrl('state_secret'); 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | protected function tearDown(): void 47 | { 48 | parent::tearDown(); 49 | Stripe::setClientId(null); 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function valueProvider() 56 | { 57 | return [ 58 | 'read_only' => [ 59 | ['scope' => 'read_only'], 60 | 'readOnly', 61 | ], 62 | 'read_write' => [ 63 | ['scope' => 'read_write'], 64 | 'readWrite', 65 | ], 66 | 'redirect_uri' => [ 67 | ['redirect_uri' => 'https://example.com'], 68 | 'redirectUri', 69 | 'https://example.com', 70 | ], 71 | 'login' => [ 72 | ['stripe_landing' => 'login'], 73 | 'login', 74 | ], 75 | 'register' => [ 76 | ['stripe_landing' => 'register'], 77 | 'register', 78 | ], 79 | 'always_prompt' => [ 80 | ['always_prompt' => 'true'], 81 | 'alwaysPrompt', 82 | ], 83 | 'user' => [ 84 | ['stripe_user' => ['email' => 'bob@example.com']], 85 | 'user', 86 | ['email' => 'bob@example.com'], 87 | ], 88 | 'stripe_user' => [ 89 | ['stripe_user' => ['email' => 'bob@example.com']], 90 | 'stripeUser', 91 | ['email' => 'bob@example.com', 'foo' => null], 92 | ], 93 | ]; 94 | } 95 | 96 | /** 97 | * @param array $expected 98 | * @param string $method 99 | * @param mixed|null $value 100 | * @dataProvider valueProvider 101 | */ 102 | public function testStandard(array $expected, $method, $value = null) 103 | { 104 | $args = !is_null($value) ? [$value] : []; 105 | $result = call_user_func_array([$this->url, $method], $args); 106 | 107 | $this->assertSame($this->url, $result, "{$method} is fluent"); 108 | $this->assertUrl('https://connect.stripe.com/oauth/authorize', $expected, "{$method}"); 109 | } 110 | 111 | /** 112 | * @param array $expected 113 | * @param string $method 114 | * @param mixed|null $value 115 | * @dataProvider valueProvider 116 | */ 117 | public function testExpress(array $expected, $method, $value = null) 118 | { 119 | $this->assertSame($this->url, $this->url->express(), 'express is fluent'); 120 | 121 | $args = !is_null($value) ? [$value] : []; 122 | $result = call_user_func_array([$this->url, $method], $args); 123 | 124 | $this->assertSame($this->url, $result, "{$method} is fluent"); 125 | $this->assertUrl('https://connect.stripe.com/express/oauth/authorize', $expected, "{$method}"); 126 | } 127 | 128 | /** 129 | * @param string $uri 130 | * @param array $params 131 | * @param string $message 132 | * @return void 133 | */ 134 | private function assertUrl($uri, array $params, $message = '') 135 | { 136 | $params = array_replace([ 137 | 'state' => 'state_secret', 138 | 'response_type' => 'code', 139 | ], $params); 140 | 141 | ksort($params); 142 | 143 | $params['client_id'] = 'my_client_id'; 144 | 145 | $expected = $uri . '?' . Util::encodeParameters($params); 146 | 147 | $this->assertSame($expected, (string) $this->url, $message); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/resources/views/oauth/error.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/tests/resources/views/oauth/error.blade.php -------------------------------------------------------------------------------- /tests/resources/views/oauth/success.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcreativity/laravel-stripe/3e85b33e568c2dcdc85254f33d125f814e8fe1e1/tests/resources/views/oauth/success.blade.php -------------------------------------------------------------------------------- /tests/stubs/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.failed", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2017-04-06", 10 | "account": "acct_00000000000000", 11 | "user_id": "acct_00000000000000", 12 | "data": { 13 | "object": { 14 | "id": "ch_00000000000000", 15 | "object": "charge", 16 | "amount": 1050, 17 | "amount_refunded": 0, 18 | "application": null, 19 | "application_fee": null, 20 | "application_fee_amount": null, 21 | "balance_transaction": "txn_00000000000000", 22 | "billing_details": { 23 | "address": { 24 | "city": null, 25 | "country": null, 26 | "line1": null, 27 | "line2": null, 28 | "postal_code": null, 29 | "state": null 30 | }, 31 | "email": null, 32 | "name": null, 33 | "phone": null 34 | }, 35 | "captured": true, 36 | "created": 1407230178, 37 | "currency": "gbp", 38 | "customer": null, 39 | "description": "Foobar", 40 | "destination": null, 41 | "dispute": null, 42 | "failure_code": null, 43 | "failure_message": null, 44 | "fraud_details": [], 45 | "invoice": null, 46 | "livemode": false, 47 | "metadata": [], 48 | "on_behalf_of": null, 49 | "order": null, 50 | "outcome": { 51 | "network_status": null, 52 | "reason": null, 53 | "risk_level": "not_assessed", 54 | "seller_message": "Payment complete.", 55 | "type": "authorized" 56 | }, 57 | "paid": false, 58 | "payment_intent": null, 59 | "payment_method": "card_00000000000000", 60 | "payment_method_details": { 61 | "card": { 62 | "brand": "visa", 63 | "checks": { 64 | "address_line1_check": null, 65 | "address_postal_code_check": null, 66 | "cvc_check": "pass" 67 | }, 68 | "country": "US", 69 | "exp_month": 8, 70 | "exp_year": 2015, 71 | "fingerprint": "hdKs5tUDfiYHfThA", 72 | "funding": "credit", 73 | "last4": "4242", 74 | "three_d_secure": null, 75 | "wallet": null 76 | }, 77 | "type": "card" 78 | }, 79 | "receipt_email": null, 80 | "receipt_number": null, 81 | "receipt_url": "https://pay.stripe.com/receipts/acct_238KrmuR0uhRnDxnilrv/ch_4X8JtIYiSwHJ0o/rcpt_EHtcPdhql5WJmhXqNUD7pGTEHCA391h", 82 | "refunded": false, 83 | "refunds": { 84 | "object": "list", 85 | "data": [], 86 | "has_more": false, 87 | "total_count": 0, 88 | "url": "/v1/charges/ch_4X8JtIYiSwHJ0o/refunds" 89 | }, 90 | "review": null, 91 | "shipping": null, 92 | "source": { 93 | "id": "card_00000000000000", 94 | "object": "card", 95 | "address_city": null, 96 | "address_country": null, 97 | "address_line1": null, 98 | "address_line1_check": null, 99 | "address_line2": null, 100 | "address_state": null, 101 | "address_zip": null, 102 | "address_zip_check": null, 103 | "brand": "Visa", 104 | "country": "US", 105 | "customer": null, 106 | "cvc_check": "pass", 107 | "dynamic_last4": null, 108 | "exp_month": 8, 109 | "exp_year": 2015, 110 | "fingerprint": "hdKs5tUDfiYHfThA", 111 | "funding": "credit", 112 | "last4": "4242", 113 | "metadata": [], 114 | "name": null, 115 | "tokenization_method": null 116 | }, 117 | "source_transfer": null, 118 | "statement_descriptor": null, 119 | "status": "failed", 120 | "transfer_data": null, 121 | "transfer_group": null 122 | } 123 | } 124 | } 125 | --------------------------------------------------------------------------------