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