├── .github └── workflows │ └── php.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── spapi.php ├── database └── migrations │ ├── 2024_08_05_154100_create_spapi_sellers_table.php │ ├── 2024_08_05_154200_create_spapi_credentials_table.php │ ├── 2024_08_05_154300_upgrade_to_laravel_spapi_v2.php │ └── 2024_09_11_135400_increase_cache_key_and_value_size.php ├── phpunit.dist.xml ├── src ├── Cache.php ├── Models │ ├── Credentials.php │ └── Seller.php └── SellingPartnerApiServiceProvider.php └── tests ├── CacheTest.php ├── MultiSellerTest.php ├── SingleSellerTest.php └── TestCase.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Validate, lint, and test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | php: [8.2, 8.3] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | coverage: none 27 | 28 | - name: Validate composer.json and composer.lock 29 | run: composer validate --strict 30 | 31 | - name: Cache Composer packages 32 | id: composer-cache 33 | uses: actions/cache@v3 34 | with: 35 | path: vendor 36 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-php- 39 | 40 | - name: Install dependencies 41 | run: composer install --prefer-dist --no-progress 42 | 43 | - name: Lint 44 | run: php vendor/bin pint --test 45 | 46 | - name: Run test suite 47 | run: vendor/bin/phpunit 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | composer.lock 3 | *.code-workspace 4 | .php-cs-fixer.cache 5 | 6 | vendor/ 7 | .idea/ 8 | .vscode/ 9 | .phpunit.cache/ 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Highside Labs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Highside Labs logo 4 | 5 |

6 | 7 |

8 | Total downloads 9 | Latest stable version 10 | License 11 |

12 | 13 | ## Selling Partner API wrapper for Laravel 14 | 15 | Simplify connecting to the Selling Partner API with Laravel. Uses [jlevers/selling-partner-api](https://github.com/jlevers/selling-partner-api) under the hood. 16 | 17 | > [!NOTE] 18 | > There is a lot of boilerplate involved in building a Selling Partner API application: setting up credential management, OAuth, infrastructure for handling feeds and reports, and more. I built an [SP API Laravel starter kit](https://tools.highsidelabs.co/starter-kit) that comes with all that functionality baked in, so you can focus on writing business logic. The starter kit uses this package, along with `jlevers/selling-partner-api`, to make developing SP API applications easier. Read the full documentation [here](https://docs.highsidelabs.co/sp-api-starter-kit). 19 | 20 | ### Related packages 21 | 22 | * [`jlevers/selling-partner-api`](https://github.com/jlevers/selling-partner-api): A PHP library for Amazon's [Selling Partner API](https://developer-docs.amazon.com/sp-api/docs). `highsidelabs/laravel-spapi` is a Laravel wrapper around `jlevers/selling-partner-api`. 23 | * [`highsidelabs/walmart-api`](https://github.com/highsidelabs/walmart-api-php): A PHP library for [Walmart's seller and supplier APIs](https://developer.walmart.com), including the Marketplace, Drop Ship Vendor, Content Provider, and Warehouse Supplier APIs. 24 | * [`highsidelabs/amazon-business-api`](https://github.com/highsidelabs/amazon-business-api): A PHP library for Amazon's [Business API](https://developer-docs.amazon.com/amazon-business/docs), with a near-identical interface to `jlevers/selling-partner-api`. 25 | 26 | --- 27 | 28 | **This package is developed and maintained by [Highside Labs](https://highsidelabs.co). If you need support integrating with Amazon's (or any other e-commerce platform's) APIs, we're happy to help! Shoot us an email at [hi@highsidelabs.co](mailto:hi@highsidelabs.co). We'd love to hear from you :)** 29 | 30 | If you've found any of our packages useful, please consider [becoming a Sponsor](https://github.com/sponsors/highsidelabs), or making a donation via the button below. We appreciate any and all support you can provide! 31 | 32 |

33 | Donate to Highside Labs 34 |

35 | 36 | --- 37 | 38 | _There is a more in-depth guide to using this package [on our blog](https://highsidelabs.co/blog/laravel-selling-partner-api)._ 39 | 40 | ## Installation 41 | 42 | ```bash 43 | $ composer require highsidelabs/laravel-spapi 44 | ``` 45 | 46 | ## Table of Contents 47 | 48 | * [Overview](#overview) 49 | * [Single-seller mode](#single-seller-mode) 50 | * [Setup](#setup) 51 | * [Usage](#usage) 52 | * [Multi-seller mode](#multi-seller-mode) 53 | * [Setup](#setup-1) 54 | * [Usage](#usage-1) 55 | * [Troubleshooting](#troubleshooting) 56 | 57 | ------ 58 | 59 | ## Overview 60 | 61 | This library has two modes: 62 | 1. **Single-seller mode**, which you should use if you only plan to make requests to the Selling Partner API with a single set of credentials (most people fall into this category, so if you're not sure, this is probably you). 63 | 2. **Multi-seller mode**, which makes it easy to make requests to the Selling Partner API from within Laravel when you have multiple sets of SP API credentials (for instance, if you operate multiple seller accounts, or operate one seller account in multiple regions). 64 | 65 | ## Single-seller mode 66 | 67 | ### Setup 68 | 69 | 1. Publish the config file: 70 | 71 | ```bash 72 | $ php artisan vendor:publish --tag="spapi-config" 73 | ``` 74 | 75 | 2. Add these environment variables to your `.env`: 76 | 77 | ```env 78 | SPAPI_LWA_CLIENT_ID= 79 | SPAPI_LWA_CLIENT_SECRET= 80 | SPAPI_LWA_REFRESH_TOKEN= 81 | 82 | # Optional 83 | # SPAPI_ENDPOINT_REGION= 84 | ``` 85 | 86 | Set `SPAPI_ENDPOINT_REGION` to the region code for the endpoint you want to use (EU for Europe, FE for Far East, or NA for North America). The default is North America. 87 | 88 | ### Usage 89 | 90 | `SellerConnector` and `VendorConnector` can be type-hinted, and the connector classes can be used to create instances of all APIs supported by [jlevers/selling-partner-api](https://github.com/jlevers/selling-partner-api#supported-api-segments). This example assumes you have access to the `Selling Partner Insights` role in your SP API app configuration (so that you can call `SellingPartnerApi\Seller\SellersV1\Api::getMarketplaceParticipations()`), _but the same principle applies to calling any other Selling Partner API endpoint._ 91 | 92 | ```php 93 | use Illuminate\Http\JsonResponse; 94 | use Saloon\Exceptions\Request\RequestException; 95 | use SellingPartnerApi\Seller\SellerConnector; 96 | 97 | class SpApiController extends Controller 98 | { 99 | public function index(SellerConnector $connector): JsonResponse 100 | { 101 | try { 102 | $api = $connector->sellersV1(); 103 | $result = $api->getMarketplaceParticipations(); 104 | return response()->json($result->json()); 105 | } catch (RequestException $e) { 106 | $response = $e->getResponse(); 107 | return response()->json($response->json(), $e->getStatus()); 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | 114 | ## Multi-seller mode 115 | 116 | ### Setup 117 | 118 | 1. Publish the config file: 119 | 120 | ```bash 121 | # Publish config/spapi.php file 122 | $ php artisan vendor:publish --provider="HighsideLabs\LaravelSpApi\SellingPartnerApiServiceProvider" 123 | ``` 124 | 125 | 2. Change the `installation_type` in `config/spapi.php` to `multi`. 126 | 127 | 3. Publish the multi-seller migrations: 128 | 129 | ```bash 130 | # Publish migrations to database/migrations/ 131 | $ php artisan vendor:publish --tag="spapi-multi-seller" 132 | ``` 133 | 134 | 135 | 4. Run the database migrations to set up the `spapi_sellers` and `spapi_credentials` tables (corresponding to the `HighsideLabs\LaravelSpApi\Models\Seller` and `HighsideLabs\LaravelSpApi\Models\Credentials` models, respectively): 136 | 137 | ```bash 138 | $ php artisan migrate 139 | ``` 140 | 141 | ### Usage 142 | 143 | First you'll need to create a `Seller`, and some `Credentials` for that seller. The `Seller` and `Credentials` models work just like any other Laravel model. 144 | 145 | ```php 146 | use HighsideLabs\LaravelSpApi\Models\Credentials; 147 | use HighsideLabs\LaravelSpApi\Models\Seller; 148 | 149 | $seller = Seller::create(['name' => 'My Seller']); 150 | $credentials = Credentials::create([ 151 | 'seller_id' => $seller->id, 152 | // You can find your selling partner ID/merchant ID by going to 153 | // https:///sw/AccountInfo/MerchantToken/step/MerchantToken 154 | 'selling_partner_id' => '', 155 | // Can be NA, EU, or FE 156 | 'region' => 'NA', 157 | // The LWA client ID and client secret for the SP API application these credentials were created with 158 | 'client_id' => 'amzn....', 159 | 'client_secret' => 'fec9/aw....', 160 | // The LWA refresh token for this seller 161 | 'refresh_token' => 'IWeB|....', 162 | ]); 163 | ``` 164 | 165 | > [!NOTE] 166 | > `client_id` and `client_secret` are nullable. If you are authorizing multiple sellers on a single SP API application, they will all use the same client ID and client secret. If you leave `client_id` and `client_secret` empty, the library will try to load those values from the `SPAPI_LWA_CLIENT_ID` and `SPAPI_LWA_CLIENT_SECRET` environment variables that are used in [single-seller mode](#single-seller-mode). That means that the single-seller credentials can effectively be used as master credentials in multi-seller mode. 167 | 168 | Once you have credentials in the database, you can use them to retrieve a `SellerConnector` instance, from which you can get an instance of any seller API: 169 | 170 | ```php 171 | use HighsideLabs\LaravelSpApi\Models\Credentials; 172 | use Illuminate\Http\JsonResponse; 173 | use Saloon\Exceptions\Request\RequestException; 174 | 175 | $creds = Credentials::first(); 176 | /** @var SellingPartnerApi\Seller\SellersV1\Api $api */ 177 | $api = $creds->sellerConnector()->sellersV1(); 178 | 179 | try { 180 | $result = $api->getMarketplaceParticipations(); 181 | $dto = $result->dto(); 182 | } catch (RequestException $e) { 183 | $responseBody = $e->getResponse()->json(); 184 | } 185 | ``` 186 | 187 | The same goes for a `VendorConnector` instance: 188 | 189 | ```php 190 | use HighsideLabs\LaravelSpApi\Models\Credentials; 191 | use Illuminate\Http\JsonResponse; 192 | use Saloon\Exceptions\Request\RequestException; 193 | 194 | $creds = Credentials::first(); 195 | /** @var SellingPartnerApi\Vendor\DirectFulfillmentShippingV1\Api $api */ 196 | $api = $creds->vendorConnector()->directFulfillmentShippingV1(); 197 | ``` 198 | 199 | ## Troubleshooting 200 | 201 | If you encounter an error like `String data, right truncated: 7 ERROR: value too long for type character varying(255)`, it's probably because you're using Laravel's database cache, which by default has a 255-character limit on cache keys and values. This library has a migration available to fix this: 202 | 203 | ```bash 204 | $ php artisan vendor:publish --tag="spapi-database-cache" 205 | $ php artisan migrate 206 | ``` 207 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highsidelabs/laravel-spapi", 3 | "type": "library", 4 | "description": "A Laravel wrapper for Amazon's Selling Partner API (via jlevers/selling-partner-api)", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "laravel", 8 | "wrapper", 9 | "selling-partner-api", 10 | "sp-api", 11 | "amazon", 12 | "ecommerce", 13 | "sdk", 14 | "rest" 15 | ], 16 | "authors": [ 17 | { 18 | "name": "Jesse Evers", 19 | "email": "jesse@highsidelabs.co" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.2", 24 | "illuminate/support": "^11.0|^12.0", 25 | "illuminate/database": "^11.0|^12.0", 26 | "illuminate/cache": "^11.0|^12.0", 27 | "jlevers/selling-partner-api": "^7.1" 28 | }, 29 | "require-dev": { 30 | "laravel/pint": "^1.17", 31 | "phpunit/phpunit": "^11.2", 32 | "orchestra/testbench": "^9.2" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "HighsideLabs\\LaravelSpApi\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "HighsideLabs\\LaravelSpApi\\Tests\\": "tests/" 42 | } 43 | }, 44 | "scripts": { 45 | "lint": "vendor/bin/pint", 46 | "test": "vendor/bin/phpunit --configuration ./phpunit.dist.xml" 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "HighsideLabs\\LaravelSpApi\\SellingPartnerApiServiceProvider" 52 | ] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/spapi.php: -------------------------------------------------------------------------------- 1 | 'single', 5 | 6 | 'single' => [ 7 | 'lwa' => [ 8 | 'client_id' => env('SPAPI_LWA_CLIENT_ID'), 9 | 'client_secret' => env('SPAPI_LWA_CLIENT_SECRET'), 10 | 'refresh_token' => env('SPAPI_LWA_REFRESH_TOKEN'), 11 | ], 12 | 13 | // Valid options are NA, EU, FE 14 | 'endpoint' => env('SPAPI_ENDPOINT_REGION', 'NA'), 15 | ], 16 | 17 | 'debug' => env('SPAPI_DEBUG', false), 18 | 'debug_file' => env('SPAPI_DEBUG_FILE'), 19 | ]; 20 | -------------------------------------------------------------------------------- /database/migrations/2024_08_05_154100_create_spapi_sellers_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->string('name')->nullable(); 19 | }); 20 | 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::drop('spapi_sellers'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_08_05_154200_create_spapi_credentials_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->timestamps(); 18 | 19 | /* 20 | * The Selling Partner ID/Merchant ID. This is returned in the OAuth response from Amazon when 21 | * authorizing a new seller on an SP API application. If self-authorizing an application, log 22 | * into Seller Central and go to the URL below to find the account's Selling Partner ID. Replace 23 | * with your region's Seller Central domain, e.g. sellercentral.amazon.com, 24 | * sellercentral-europe.amazon.com, etc: 25 | * 26 | * https:///sw/AccountInfo/MerchantToken/step/MerchantToken 27 | */ 28 | $table->string('selling_partner_id')->unique(); 29 | 30 | // The SP API region that these credentials are for 31 | $table->enum('region', Region::values()); 32 | 33 | // The app credentials that the the refresh token was created with 34 | $table->string('client_id')->nullable(); 35 | $table->string('client_secret')->nullable(); 36 | 37 | // The LWA refresh token for this set of credentials 38 | $table->string('refresh_token', 511); 39 | 40 | // The seller these credentials are associated with 41 | $table->foreignId('seller_id')->constrained('spapi_sellers'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | */ 48 | public function down(): void 49 | { 50 | Schema::drop('spapi_credentials'); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /database/migrations/2024_08_05_154300_upgrade_to_laravel_spapi_v2.php: -------------------------------------------------------------------------------- 1 | dropColumn($col); 20 | }); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | // Since the AWS columns were config-dependent to begin with, and the config 31 | // option that determined their presence is deprecated, we're not defining 32 | // a down migration. 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2024_09_11_135400_increase_cache_key_and_value_size.php: -------------------------------------------------------------------------------- 1 | string('key', 511)->change(); 15 | $table->string('value', 2559)->change(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('cache', function (Blueprint $table) { 25 | // This will throw an error if there are values longer than 255 characters in the cache table, 26 | // but if we automatically truncated them there would be silent data loss 27 | $table->string('key', 255)->change(); 28 | $table->string('value', 255)->change(); 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /phpunit.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | src 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | credsTag = "creds$credsId"; 22 | } 23 | 24 | public function get(string $key): AccessTokenAuthenticator|false 25 | { 26 | if (self::isTaggableCache()) { 27 | $token = LaravelCache::tags([self::TAG, $this->credsTag])->get($key); 28 | } else { 29 | $token = LaravelCache::get($key); 30 | } 31 | 32 | if (! $token) { 33 | return false; 34 | } 35 | 36 | return unserialize($token); 37 | } 38 | 39 | public function set(string $key, AccessTokenAuthenticator $authenticator): void 40 | { 41 | $ttl = $authenticator->getExpiresAt()->getTimestamp() - (new DateTimeImmutable)->getTimestamp(); 42 | if (self::isTaggableCache()) { 43 | LaravelCache::tags([self::TAG, $this->credsTag])->put($key, serialize($authenticator), $ttl); 44 | } else { 45 | LaravelCache::put($key, serialize($authenticator), $ttl); 46 | } 47 | } 48 | 49 | public function forget(string $key): void 50 | { 51 | LaravelCache::tags([self::TAG, $this->credsTag])->forget($key); 52 | } 53 | 54 | public function clearForCreds(): void 55 | { 56 | if (self::isTaggableCache()) { 57 | LaravelCache::tags([self::TAG, $this->credsTag])->flush(); 58 | } 59 | } 60 | 61 | public function clear(): void 62 | { 63 | if (self::isTaggableCache()) { 64 | LaravelCache::tags([self::TAG])->flush(); 65 | } 66 | } 67 | 68 | private static function isTaggableCache(): bool 69 | { 70 | return LaravelCache::getStore() instanceof TaggableStore; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Models/Credentials.php: -------------------------------------------------------------------------------- 1 | client_id ?? config('spapi.single.lwa.client_id'), 37 | clientSecret: $this->client_secret ?? config('spapi.single.lwa.client_secret'), 38 | refreshToken: $this->refresh_token, 39 | endpoint: Endpoint::byRegion($this->region), 40 | dataElements: $dataElements, 41 | delegatee: $delegatee, 42 | authenticationClient: $authenticationClient, 43 | cache: new Cache($this->id), 44 | ); 45 | 46 | static::debug($connector); 47 | 48 | return $connector; 49 | } 50 | 51 | /** 52 | * Create a VendorConnector instance from these credentials. 53 | */ 54 | public function vendorConnector( 55 | ?array $dataElements = [], 56 | ?string $delegatee = null, 57 | ?Client $authenticationClient = null 58 | ): VendorConnector { 59 | $connector = SellingPartnerApi::vendor( 60 | clientId: $this->client_id ?? config('spapi.single.lwa.client_id'), 61 | clientSecret: $this->client_secret ?? config('spapi.single.lwa.client_secret'), 62 | refreshToken: $this->refresh_token, 63 | endpoint: Endpoint::byRegion($this->region), 64 | dataElements: $dataElements, 65 | delegatee: $delegatee, 66 | authenticationClient: $authenticationClient, 67 | cache: new Cache($this->id), 68 | ); 69 | 70 | static::debug($connector); 71 | 72 | return $connector; 73 | } 74 | 75 | /** 76 | * Get the Seller that owns the Credentials. 77 | */ 78 | public function seller(): BelongsTo 79 | { 80 | return $this->belongsTo(Seller::class); 81 | } 82 | 83 | /** 84 | * Manage debug settings on API connector class. 85 | */ 86 | protected static function debug(SellingPartnerApi $connector): void 87 | { 88 | if (config('spapi.debug')) { 89 | if (config('spapi.debug_file')) { 90 | $connector->debugToFile(config('spapi.debug_file')); 91 | } else { 92 | $connector->debug(); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Perform any actions required after the model boots. 99 | */ 100 | protected static function booted(): void 101 | { 102 | // Bust the cache when the model is updated, in case the access token 103 | // is no longer valid for the updated credentials. 104 | static::updating(function (self $credentials) { 105 | $cache = new Cache($credentials->id); 106 | $cache->clearForCreds(); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Models/Seller.php: -------------------------------------------------------------------------------- 1 | hasMany(Credentials::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SellingPartnerApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([__DIR__.'/../config/spapi.php' => config_path('spapi.php')], 'spapi-config'); 19 | 20 | // Publish spapi_sellers and spapi_credentials migrations 21 | $migrationsDir = __DIR__.'/../database/migrations'; 22 | $sellersMigrationFile = '2024_08_05_154100_create_spapi_sellers_table.php'; 23 | $credentialsMigrationFile = '2024_08_05_154200_create_spapi_credentials_table.php'; 24 | $this->publishesMigrations([ 25 | "$migrationsDir/$sellersMigrationFile" => database_path("migrations/$sellersMigrationFile"), 26 | "$migrationsDir/$credentialsMigrationFile" => database_path("migrations/$credentialsMigrationFile"), 27 | ], 'spapi-multi-seller'); 28 | 29 | $dbCacheMigrationFile = '2024_09_11_135400_increase_cache_key_and_value_size.php'; 30 | $this->publishesMigrations([ 31 | "$migrationsDir/$dbCacheMigrationFile" => database_path("migrations/$dbCacheMigrationFile"), 32 | ], 'spapi-database-cache'); 33 | 34 | // Don't offer the option to publish the package version upgrade migration unless this is a multi-seller 35 | // installation that was using dynamic AWS credentials (a feature that is now deprecated/irrelevant) 36 | if (config('spapi.installation_type') === 'multi' && config('spapi.aws.dynamic')) { 37 | $v2MigrationFile = '2024_08_05_154300_upgrade_to_laravel_spapi_v2.php'; 38 | $this->publishesMigrations([ 39 | "$migrationsDir/$v2MigrationFile" => database_path("migrations/$v2MigrationFile"), 40 | ], 'spapi-v2-upgrade'); 41 | } 42 | } 43 | 44 | /** 45 | * Register bindings in the container. 46 | */ 47 | public function register(): void 48 | { 49 | if (config('spapi.installation_type') === 'single') { 50 | $creds = new Credentials([ 51 | 'client_id' => config('spapi.single.lwa.client_id'), 52 | 'client_secret' => config('spapi.single.lwa.client_secret'), 53 | 'refresh_token' => config('spapi.single.lwa.refresh_token'), 54 | 'region' => config('spapi.single.endpoint'), 55 | ]); 56 | // To give the cache an ID to work with 57 | $creds->id = 1; 58 | 59 | $this->app->bind(SellerConnector::class, fn () => $creds->sellerConnector()); 60 | $this->app->bind(VendorConnector::class, fn () => $creds->vendorConnector()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/CacheTest.php: -------------------------------------------------------------------------------- 1 | 'seller-1']); 23 | $creds = Credentials::create([ 24 | 'seller_id' => $seller->id, 25 | 'selling_partner_id' => 'spid01', 26 | 'region' => 'NA', 27 | 'client_id' => 'client-id', 28 | 'client_secret' => 'client-secret', 29 | 'refresh_token' => 'refresh-token', 30 | ]); 31 | 32 | $this->cache = new Cache($creds->id); 33 | } 34 | 35 | public function testStoresToken(): void 36 | { 37 | $expiration = new DateTimeImmutable('1 hour'); 38 | $token = new AccessTokenAuthenticator('access-token', expiresAt: $expiration); 39 | $this->cache->set('token-1', $token); 40 | 41 | $fetched = $this->cache->get('token-1'); 42 | $this->assertEquals($token, $fetched); 43 | } 44 | 45 | public function testExpiresStoredToken(): void 46 | { 47 | $token = new AccessTokenAuthenticator('access-token', expiresAt: new \DateTimeImmutable('-1 hour')); 48 | $this->cache->set('token-1', $token); 49 | 50 | $fetched = $this->cache->get('token-1'); 51 | $this->assertFalse($fetched); 52 | } 53 | 54 | public function testDeletesKey(): void 55 | { 56 | $this->cache->set('token-1', new AccessTokenAuthenticator('access-token', expiresAt: new \DateTimeImmutable('+1 hour'))); 57 | $this->cache->forget('token-1'); 58 | $this->assertFalse($this->cache->get('token-1')); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/MultiSellerTest.php: -------------------------------------------------------------------------------- 1 | seller = Seller::create(['name' => 'seller-1']); 24 | $this->creds = Credentials::create([ 25 | 'seller_id' => $this->seller->id, 26 | 'selling_partner_id' => 'spapi01', 27 | 'client_id' => 'client-id-1', 28 | 'client_secret' => 'client-secret-1', 29 | 'refresh_token' => 'refresh-token-1', 30 | 'region' => 'NA', 31 | ]); 32 | } 33 | 34 | public function testCanMakeSellerApis(): void 35 | { 36 | $sellerConnector = $this->creds->sellerConnector(); 37 | $api = $sellerConnector->sellersV1(); 38 | 39 | $this->assertInstanceOf(SellersV1\Api::class, $api); 40 | $this->assertEquals('client-id-1', $sellerConnector->clientId); 41 | $this->assertEquals(Endpoint::NA, $sellerConnector->endpoint); 42 | } 43 | 44 | public function testCanMakeVendorApis(): void 45 | { 46 | $vendorConnector = $this->creds->vendorConnector(); 47 | $api = $vendorConnector->directFulfillmentShippingV1(); 48 | 49 | $this->assertInstanceOf(DirectFulfillmentShippingV1\Api::class, $api); 50 | $this->assertEquals('client-id-1', $vendorConnector->clientId); 51 | $this->assertEquals(Endpoint::NA, $vendorConnector->endpoint); 52 | } 53 | 54 | public function testCanMakeSellerApiWithNoClientCredentials(): void 55 | { 56 | $creds = Credentials::create([ 57 | 'seller_id' => $this->seller->id, 58 | 'selling_partner_id' => 'spapi02', 59 | 'refresh_token' => 'refresh-token-2', 60 | 'region' => 'EU', 61 | ]); 62 | 63 | $sellerConnector = $creds->sellerConnector(); 64 | 65 | $this->assertEquals('client-id', $sellerConnector->clientId); 66 | $this->assertEquals('client-secret', $sellerConnector->clientSecret); 67 | $this->assertEquals(Endpoint::EU, $sellerConnector->endpoint); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/SingleSellerTest.php: -------------------------------------------------------------------------------- 1 | sellerConnector = $this->app->make(SellerConnector::class); 24 | $this->vendorConnector = $this->app->make(VendorConnector::class); 25 | } 26 | 27 | public function testUsesCorrectCredentials(): void 28 | { 29 | $this->assertEquals('client-id', $this->sellerConnector->clientId); 30 | $this->assertEquals(Endpoint::EU, $this->sellerConnector->endpoint); 31 | 32 | $this->assertEquals('client-id', $this->vendorConnector->clientId); 33 | $this->assertEquals(Endpoint::EU, $this->vendorConnector->endpoint); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('spapi', $spapiConfig); 26 | } 27 | 28 | protected function defineDatabaseMigrations(): void 29 | { 30 | // Migrations cannot be loaded via artisan($this, 'vendor:publish', ['--tag' => 'spapi-multi-seller']), 31 | // because Laravel rewrites their timestamps every time they're published, which means that Testbench 32 | // duplicates them for every test that's run 33 | $this->loadMigrationsFrom([ 34 | __DIR__.'/../database/migrations/2024_08_05_154100_create_spapi_sellers_table.php', 35 | __DIR__.'/../database/migrations/2024_08_05_154200_create_spapi_credentials_table.php', 36 | ]); 37 | } 38 | 39 | /** 40 | * Get package providers. 41 | */ 42 | protected function getPackageProviders($app): array 43 | { 44 | return [SellingPartnerApiServiceProvider::class]; 45 | } 46 | } 47 | --------------------------------------------------------------------------------