diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml index f8a0fe8..333ffff 100644 --- a/.github/workflows/analyse.yml +++ b/.github/workflows/analyse.yml @@ -10,11 +10,9 @@ jobs: matrix: os: [ubuntu-latest] php: [8.3] - laravel: [10.*, 11.*] + laravel: [11.*] stability: [prefer-stable] include: - - laravel: 10.* - testbench: 8.* - laravel: 11.* testbench: 9.* diff --git a/generate-changelog.yml b/.github/workflows/changelog.yml similarity index 98% rename from generate-changelog.yml rename to .github/workflows/changelog.yml index aca737e..161eadb 100644 --- a/generate-changelog.yml +++ b/.github/workflows/changelog.yml @@ -5,7 +5,7 @@ on: types: [ published, edited, deleted ] jobs: - update: + generate: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c85cbf7..0d9d3b0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,11 +10,9 @@ jobs: matrix: os: [ubuntu-latest] php: [8.3] - laravel: [10.*, 11.*] + laravel: [11.*] stability: [prefer-stable] include: - - laravel: 10.* - testbench: 8.* - laravel: 11.* testbench: 9.* diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml deleted file mode 100644 index 49a863e..0000000 --- a/.github/workflows/generate-changelog.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: "Update Changelog" - -on: - release: - types: [ published, edited, deleted ] - -jobs: - update: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.target_commitish }} - - - name: Generate changelog - uses: justbetter/generate-changelogs-action@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - repository: ${{ github.repository }} - - - name: Commit CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: ${{ github.event.release.target_commitish }} - commit_message: Update CHANGELOG - file_pattern: CHANGELOG.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf6eb0c..c502770 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,12 +9,10 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.1, 8.2, 8.3] - laravel: [10.*, 11.*] - stability: [prefer-lowest, prefer-stable] + php: [8.2, 8.3] + laravel: [11.*] + stability: [prefer-stable] include: - - laravel: 10.* - testbench: 8.* - laravel: 11.* testbench: 9.* exclude: diff --git a/README.md b/README.md index cb406a7..e8a1fb4 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ This package can: - Only update prices in Magento when are modified. i.e. when you retrieve the same price ten times it only updates once to Magento - Search for missing prices in Magento - Automatically stop syncing when updating fails +- Supports Magento 2 async bulk requests for updating using [Laravel Magento Async](https://github.com/justbetter/laravel-magento-async) - Logs activities using [Spatie activitylog](https://github.com/spatie/laravel-activitylog) -- Logs errors using [JustBetter Error Logger](https://github.com/justbetter/laravel-error-logger) - Checks if Magento products exist using [JustBetter Magento Products](https://github.com/justbetter/laravel-magento-products) > Also using customer specific prices? [See our other package!](https://github.com/justbetter/laravel-magento-customer-prices) @@ -33,23 +33,13 @@ This package can: Require this package: `composer require justbetter/laravel-magento-prices` -Publish the config: -``` -php artisan vendor:publish --provider="JustBetter\MagentoPrices\ServiceProvider" --tag="config" -``` - -Publish the activity log's migrations: -``` -php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" -``` +Publish the config: `php artisan vendor:publish --provider="JustBetter\MagentoPrices\ServiceProvider" --tag="config"` -Publish the batches table migration: -``` -php artisan queue:batches-table -``` +Publish the activity log's migrations: `php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"` -Run migrations. +Run migrations. `php artisan migrate` +> **_TIP:_** All actions in this package are run via jobs, we recommend Laravel Horizon or another queueing system to run these ### Laravel Nova @@ -58,116 +48,145 @@ We have a [Laravel Nova integration](https://github.com/justbetter/laravel-magen ## Usage Add the following commands to your scheduler: + ```php command(\JustBetter\MagentoPrices\Commands\SyncPricesCommand::class)->everyMinute(); + $schedule->command(\JustBetter\MagentoPrices\Commands\ProcessPricesCommand::class)->everyMinute(); - $schedule->command(\JustBetter\MagentoPrices\Commands\RetrievePricesCommand::class)->daily(); - // Or for example - $schedule->command(\JustBetter\MagentoPrices\Commands\RetrievePricesCommand::class)->weekly(); // Retrieve all weekly - $schedule->command('price:retrieve --date=today')->dailyAt('23:00'); // Retrieve updated daily + $schedule->command(\JustBetter\MagentoPrices\Commands\Retrieval\RetrieveAllPricesCommand::class)->daily(); + $schedule->command(\JustBetter\MagentoPrices\Commands\Retrieval\RetrieveAllPricesCommand::class, ['from' => 'now -2 hours'])->hourly(); // Retrieve updated } ``` - ### Retrieving Prices -In order to retrieve prices you have to create two classes. One to retrieve skus and one to retrieve the price(s) per sku. +This package works with a repository that retrieves prices per SKU which you have to implement. -#### Price retriever +#### Repository -This class is responsible for retrieving prices for products. Your class must extend `\JustBetter\MagentoPrices\Retriever\PriceRetriever`. -Your class must return a `PriceData` object or `null`. -The `PriceData` object is a wrapper around three collections: -- Base prices -- Tier prices -- Special Prices +This class is responsible for retrieving prices for products, retrieving sku's and settings. +Your class must extend `\JustBetter\MagentoPrices\Repository\Repository` and implement the `retrieve` method. +If there is no price for the SKU you may return `null`. In all other cases you need to return a `PriceData` object which contains four elements: +- `sku` Required +- `base_prices` Optional, array of base prices +- `tier_prices` Optional, array of tier prices +- `special_prices` Optional, array of special prices -See the classes `BasePriceData` and `TierPriceData` in the `JustBetter\MagentoPrices\Data` namespace on how to use them. +The formats of the price arrays follows Magento's API. +You can view the rules in the `PriceData` class to get an idea of what you need to provide. -For example: +##### Example ```php + moneyHelper->getMoney($this->externalService->getBasePrice($sku)) - ); - - $tierPrices = $this->externalService->getTierPrices($sku) - ->mapInto(TierPriceData::class); - - $specialPrices = $this->externalService->getSpecialPrices($sku) - ->mapInto(SpecialPriceData::class); - - return new PriceData($sku, collect([$basePrice]), $tierPrices, $specialPrices); + return PriceData::of([ + 'sku' => $sku, + 'base_prices' => [ + [ + 'store_id' => 0, + 'price' => 10, + ], + [ + 'store_id' => 2, + 'price' => 19, + ], + ], + 'tier_prices' => [ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + [ + 'website_id' => 0, + 'customer_group' => '4040', + 'price_type' => 'group_2', + 'quantity' => 1, + 'price' => 7, + ], + ], + 'special_prices' => [ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], + ]); } } ``` -Then register your retriever in the config file `config/magento-prices.php`: +### Retrieving SKU's + +By default, the `Repository` that you are extending will retrieve the SKU's from [justbetter/laravel-magento-products](https://github.com/justbetter/laravel-magento-products). +If you wish to use this you have to add the commands to your scheduler to automatically import products. + +If you have another source for your SKU's you may implement the `skus` method yourself. +It accepts an optional carbon instance to only retrieve modified stock. ```php [ - 'price' => ExamplePriceRetriever::class, - ], -``` - -You can retrieve the price by running: `php artisan price:retrieve {sku}` +namespace App\Integrations\MagentoPrices; -##### Storing Money +use JustBetter\MagentoPrices\Repositories\Repository; +use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; -We use Brick/money for storing prices, to create a price use: -```php - $basePrice = new BasePriceData( - Money::of(10, config('laravel-magento-prices.currency')) - ); +class MyPriceRepository implements Repository +{ + public function skus(?Carbon $from = null): ?Collection + { + return collect(['sku_1', 'sku_2']); + } +} ``` -There is a helper for that which adds precision, context and the rounding mode from the config: `JustBetter\MagentoPrices\Helpers\MoneyHelper` - -##### Example - -To help you get started you can look at the `\JustBetter\MagentoPrices\Retriever\DummyPriceRetriever` +### Configuring the repository -#### SKU Retriever +The repository class has a couple of settings that you can adjust: -In order to know what SKU's to retrieve prices for you have to create a SKU retriever class. This class must extend `\JustBetter\MagentoPrices\Retriever\SkuRetriever`. +```php +class BaseRepository +{ + // How many prices may be retrieved at once when the process job runs + protected int $retrieveLimit = 250; -It is required to have a method that retrieves all skus. You can optionally implement the `retrieveByDate` method to retrieve updated skus. + // How many prices may be updated at once when the process job runs + protected int $updateLimit = 250; -And example can be found in `\JustBetter\MagentoPrices\Retriever\DummySkuRetriever` + // How many times an update to Magento may fail before it stops trying + protected int $failLimit = 3; +} +``` -Don't forget to register your retriever in the config file `config/magento-prices.php`: +After you've created and configured the repository you have to set it in your configuration file: ```php [ - 'sku' => MyAwewsomeSkuRetriever::class, - ], + 'repository' => \App\Integrations\MagentoPrices\MyPriceRepository::class, +]; ``` ### Checking for missing prices @@ -175,70 +194,24 @@ return [ There is a build in action that checks all products in Magento where there is no price or the price is zero. For each product it will automatically start an update or retrieve. -You can run this with the command: `php artisan price:missing` - - -### Syncing - -The `php artisan price:sync` command will check the `retrieve` and `update` flags and dispatch jobs to retrieve/update the prices. -In order to not overload your price source or Magento you can set limits in the config file. -```php - 25, - - /* How many prices update jobs may be dispatched per sync */ - 'update_limit' => 100, -]; -``` - -#### Long Waits - -The sync limits the amount of products that are retrieved/updated each sync. -This may result in long waits if not properly configured for the amount of updates you get. - -To detect this you can add the `\JustBetter\MagentoPrices\Commands\MonitorWaitTimesCommand` to your schedule. -This will fire the `\JustBetter\MagentoPrices\Events\LongWaitDetectedEvent` event in which you can for example trigger more updates or send a notification. - -You can configure the limits of when the event will be fired in the config: -```php - [ - /* Max wait time in minutes, if exceeded the LongWaitDetected event is dispatched */ - 'retrieval_max_wait' => 30, - - /* Max wait time in minutes, if exceeded the LongWaitDetected event is dispatched */ - 'update_max_wait' => 30, - ] -]; -``` +You can run this with the command: `php artisan magento-prices:process-missing-prices` ### Handling failures When an update fails it will try again. A fail counter is stored with the model which is increased at each failure. -A common failure is a missing required attribute in Magento. - -In the config you can specify how many times the update may be attempted: -```php - 5, -]; -``` -> *Note* This applies to all three types of updates. Base, tier and special. +In the repository you can specify how many times the update may be attempted ### Events Events that are dispatched by this package are: -- [`\JustBetter\MagentoPrices\Events\LongWaitDetectedEvent`](#long-waits) - `\JustBetter\MagentoPrices\Events\UpdatedPriceEvent` - Triggered when a price is updated +### Async bulk + +In order to drastically decrease the amount of requests to Magento you can enable the `async` option in the configuration file. +This will use [Laravel Magento Async](https://github.com/justbetter/laravel-magento-async) to send the update requets. +Do not forget to follow the installation guide on that package. + ## Quality To ensure the quality of this package, run the following command: diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..77ae19b --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,21 @@ +# Upgrade Guide + +## 1.x to 2.x + +2.x introduces a complete refactor of the package structure. + +A few highlights: +- Simplified implementation +- Support updating via Magento 2 bulk async requests +- Removed error logger, replaced with activity log +- Dropped support for Laravel 10 + +### Update your project + +The price retriever and SKU retriever classes all have been merged into a single repository class. +Refer to the readme on how to implement this. + +The configuration file has been stripped, most of the configuration is now done in the repository class. + +A lot of classes have been renamed, be sure to update your scheduler and check all classes that you use. +The price model has been renamed from `MagentoPrice` to `Price`. diff --git a/composer.json b/composer.json index 2b967dc..9cf2075 100644 --- a/composer.json +++ b/composer.json @@ -4,20 +4,20 @@ "type": "package", "license": "MIT", "require": { - "php": "^8.1", - "brick/money": "^0.7|^0.8", + "php": "^8.2", "justbetter/laravel-magento-client": "^2.4", "justbetter/laravel-magento-products": "^1.4", - "laravel/framework": "^10.0|^11.0", + "justbetter/laravel-magento-async": "^1.0", + "laravel/framework": "^11.0", "spatie/laravel-activitylog": "^4.8" }, "require-dev": { "doctrine/dbal": "^3.6", "larastan/larastan": "^2.9", - "laravel/pint": "^1.6", - "orchestra/testbench": "^8.0|^9.0", + "laravel/pint": "^1.17", + "orchestra/testbench": "^9.0", "phpstan/phpstan-mockery": "^1.1", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "authors": [ { diff --git a/config/magento-prices.php b/config/magento-prices.php index 762f458..45fcb80 100644 --- a/config/magento-prices.php +++ b/config/magento-prices.php @@ -1,40 +1,11 @@ [ - 'sku' => DummySkuRetriever::class, - 'price' => DummyPriceRetriever::class, - ], - - 'currency' => 'EUR', - 'precision' => 4, - 'rounding_mode' => RoundingMode::HALF_UP, - - /* How many times can a price update failed before being cancelled */ - 'fail_count' => 5, - - /* How many price retrieval jobs may be dispatched per sync */ - 'retrieve_limit' => 25, - - /* How many prices update jobs may be dispatched per sync */ - 'update_limit' => 100, + 'repository' => \JustBetter\MagentoPrices\Repository\Repository::class, /* Queue for the jobs to run on */ 'queue' => 'default', - 'monitor' => [ - /* Max wait time in minutes, if exceeded the LongWaitDetected event is dispatched */ - 'retrieval_max_wait' => 30, - - /* Max wait time in minutes, if exceeded the LongWaitDetected event is dispatched */ - 'update_max_wait' => 30, - ], - - /* Send stock updates using Magento 2's async endpoints, a configured message queue in Magento is required for this */ + /* Send updates using Magento 2's async bulk endpoints, a configured message queue in Magento is required for this */ 'async' => false, ]; diff --git a/database/migrations/2024_07_26_163000_magento_prices_add_checksum_column.php b/database/migrations/2024_07_26_163000_magento_prices_add_checksum_column.php new file mode 100644 index 0000000..bf912dc --- /dev/null +++ b/database/migrations/2024_07_26_163000_magento_prices_add_checksum_column.php @@ -0,0 +1,20 @@ +string('checksum')->nullable()->after('update'); + }); + } + + public function down(): void + { + Schema::dropColumns('magento_prices', ['checksum']); + } +}; diff --git a/database/migrations/2024_07_30_153000_magento_prices_drop_unused_columns.php b/database/migrations/2024_07_30_153000_magento_prices_drop_unused_columns.php new file mode 100644 index 0000000..a62392a --- /dev/null +++ b/database/migrations/2024_07_30_153000_magento_prices_drop_unused_columns.php @@ -0,0 +1,20 @@ +boolean('has_tier')->default(false)->after('tier_prices'); + }); + } +}; diff --git a/phpstan.neon b/phpstan.neon index 2c82c41..53ccd28 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,9 +6,7 @@ parameters: paths: - src - tests - level: 6 - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false + level: 8 ignoreErrors: - - '#Unable to resolve the template type TKey in call to function collect#' - - '#Unable to resolve the template type TValue in call to function collect#' + - identifier: missingType.iterableValue + - identifier: missingType.generics diff --git a/src/Actions/CheckTierDuplicates.php b/src/Actions/CheckTierDuplicates.php deleted file mode 100644 index 77a8a75..0000000 --- a/src/Actions/CheckTierDuplicates.php +++ /dev/null @@ -1,51 +0,0 @@ -where('storeId', $tierPrice->storeId) - ->where('quantity', $tierPrice->quantity) - ->where('groupId', $tierPrice->groupId); - - if ($matching->count() == 1) { - continue; - } - - /** @var ?MagentoPrice $model */ - $model = MagentoPrice::query()->firstWhere('sku', '=', $sku); - - activity() - ->when($model !== null, fn (ActivityLogger $logger) => $logger->on($model)) - ->useLog('error') - ->withProperties([ - 'sku' => $sku, - 'duplicate' => $matching->toArray(), - ]) - ->log("Duplicate tier prices found for $sku"); - - throw new DuplicateTierPriceException($sku, $matching); - } - } - - public static function bind(): void - { - app()->singleton(ChecksTierDuplicates::class, static::class); - } -} diff --git a/src/Actions/DeterminePricesEqual.php b/src/Actions/DeterminePricesEqual.php deleted file mode 100644 index 7e5d882..0000000 --- a/src/Actions/DeterminePricesEqual.php +++ /dev/null @@ -1,111 +0,0 @@ -basePricesEqual($a->basePrices, $b->basePrices)) { - return false; - } - - if (! $this->tierPricesEqual($a->tierPrices, $b->tierPrices)) { - return false; - } - - if (! $this->specialPriceEqual($a->specialPrices, $b->specialPrices)) { - return false; - } - - return true; - } - - protected function basePricesEqual(Collection $a, Collection $b): bool - { - if ($a->count() !== $b->count()) { - return false; - } - - /** @var BasePriceData $basePrice */ - foreach ($a as $basePrice) { - /** @var ?BasePriceData $matchingPrice */ - $matchingPrice = $b->where('storeId', '=', $basePrice->storeId)->first(); - - if ($matchingPrice === null) { - return false; - } - - if (! $basePrice->equals($matchingPrice)) { - return false; - } - } - - return true; - } - - protected function tierPricesEqual(Collection $a, Collection $b): bool - { - if ($a->count() !== $b->count()) { - return false; - } - - /** @var TierPriceData $tierPrice */ - foreach ($a as $tierPrice) { - /** @var ?TierPriceData $matchingPrice */ - $matchingPrice = $b - ->where('groupId', '=', $tierPrice->groupId) - ->where('storeId', '=', $tierPrice->storeId) - ->where('quantity', '=', $tierPrice->quantity) - ->where('priceType', '=', $tierPrice->priceType) - ->first(); - - if ($matchingPrice === null) { - return false; - } - - if (! $tierPrice->equals($matchingPrice)) { - return false; - } - } - - return true; - } - - protected function specialPriceEqual(Collection $a, Collection $b): bool - { - if ($a->count() !== $b->count()) { - return false; - } - - /** @var SpecialPriceData $specialPrice */ - foreach ($a as $specialPrice) { - /** @var ?SpecialPriceData $matchingPrice */ - $matchingPrice = $b - ->where('storeId', '=', $specialPrice->storeId) - ->first(); - - if ($matchingPrice === null) { - return false; - } - - if (! $specialPrice->equals($matchingPrice)) { - return false; - } - } - - return true; - } - - public static function bind(): void - { - app()->singleton(DeterminesPricesEqual::class, static::class); - } -} diff --git a/src/Actions/FindProductsWithMissingPrices.php b/src/Actions/FindProductsWithMissingPrices.php deleted file mode 100644 index 146c849..0000000 --- a/src/Actions/FindProductsWithMissingPrices.php +++ /dev/null @@ -1,43 +0,0 @@ -select(['sku', 'price', 'type_id']) - ->get(); - - $products = $this->magento->lazy('products', $searchCriteria); - - foreach ($products as $product) { - if ( - (array_key_exists('price', $product) && floatval($product['price']) > 0) || - (array_key_exists('type_id', $product) && $product['type_id'] != 'simple') - ) { - continue; - } - - yield $product['sku']; - } - }); - } - - public static function bind(): void - { - app()->singleton(FindsProductsWithMissingPrices::class, static::class); - } -} diff --git a/src/Actions/MonitorWaitTimes.php b/src/Actions/MonitorWaitTimes.php deleted file mode 100644 index d4fe23e..0000000 --- a/src/Actions/MonitorWaitTimes.php +++ /dev/null @@ -1,55 +0,0 @@ -monitorRetrievals(); - $this->monitorUpdates(); - } - - protected function monitorRetrievals(): void - { - $retrievalsPerMinute = config('magento-prices.retrieve_limit'); - $maxWaitTime = config('magento-prices.monitor.retrieval_max_wait'); - - $waitingCount = MagentoPrice::query() - ->where('sync', '=', true) - ->where('retrieve', '=', true) - ->count(); - - $wait = $waitingCount / $retrievalsPerMinute; - - if ($wait > $maxWaitTime) { - event(new LongWaitDetectedEvent('retrieve', $wait)); - } - } - - protected function monitorUpdates(): void - { - $retrievalsPerMinute = config('magento-prices.update_limit'); - $maxWaitTime = config('magento-prices.monitor.update_max_wait'); - - $waitingCount = MagentoPrice::query() - ->where('sync', '=', true) - ->where('update', '=', true) - ->count(); - - $wait = $waitingCount / $retrievalsPerMinute; - - if ($wait > $maxWaitTime) { - event(new LongWaitDetectedEvent('update', $wait)); - } - } - - public static function bind(): void - { - app()->singleton(MonitorsWaitTimes::class, static::class); - } -} diff --git a/src/Actions/ProcessPrice.php b/src/Actions/ProcessPrice.php deleted file mode 100644 index a072c63..0000000 --- a/src/Actions/ProcessPrice.php +++ /dev/null @@ -1,45 +0,0 @@ -validate(); - - $priceModel = $price->getModel(); - $prices = $price; - - $currentPrices = $priceModel->getData(); - - $priceModel->base_prices = $prices->basePrices; - $priceModel->tier_prices = $prices->tierPrices; - $priceModel->special_prices = $prices->specialPrices; - - $priceModel->last_retrieved = now(); - $priceModel->retrieve = false; - - $priceModel->update = $forceUpdate || ! $currentPrices->equals($price); - - if (! $priceModel->sync && $priceModel->update && $this->checksMagentoExistence->exists($priceModel->sku)) { - $priceModel->sync = true; - } - - $priceModel->save(); - } - - public static function bind(): void - { - app()->singleton(ProcessesPrice::class, static::class); - } -} diff --git a/src/Actions/ProcessPrices.php b/src/Actions/ProcessPrices.php new file mode 100644 index 0000000..642ac68 --- /dev/null +++ b/src/Actions/ProcessPrices.php @@ -0,0 +1,55 @@ +where('sync', '=', true) + ->where('retrieve', '=', true) + ->select(['sku']) + ->take($repository->retrieveLimit()) + ->get() + ->each(fn (Price $price): PendingDispatch => RetrievePriceJob::dispatch($price->sku)); + + if (config('magento-prices.async')) { + $prices = Price::query() + ->where('sync', '=', true) + ->where('update', '=', true) + ->whereHas('product', function (Builder $query): void { + $query->where('exists_in_magento', '=', true); + }) + ->select(['id', 'sku']) + ->take($repository->updateLimit()) + ->get(); + + UpdatePricesAsyncJob::dispatch($prices); + } else { + Price::query() + ->where('sync', '=', true) + ->where('update', '=', true) + ->select(['id', 'sku']) + ->take($repository->updateLimit()) + ->get() + ->each(fn (Price $price): PendingDispatch => UpdatePriceJob::dispatch($price)); + } + } + + public static function bind(): void + { + app()->singleton(ProcessesPrices::class, static::class); + } +} diff --git a/src/Actions/Retrieval/RetrieveAllPrices.php b/src/Actions/Retrieval/RetrieveAllPrices.php new file mode 100644 index 0000000..ec4c860 --- /dev/null +++ b/src/Actions/Retrieval/RetrieveAllPrices.php @@ -0,0 +1,24 @@ +skus($from)->each(fn (string $sku): PendingDispatch => RetrievePriceJob::dispatch($sku)); + } + + public static function bind(): void + { + app()->singleton(RetrievesAllPrices::class, static::class); + } +} diff --git a/src/Actions/Retrieval/RetrievePrice.php b/src/Actions/Retrieval/RetrievePrice.php new file mode 100644 index 0000000..4f64bd4 --- /dev/null +++ b/src/Actions/Retrieval/RetrievePrice.php @@ -0,0 +1,33 @@ +retrieve($sku); + + if ($priceData === null) { + Price::query() + ->where('sku', '=', $sku) + ->update(['retrieve' => false]); + + return; + } + + SavePriceJob::dispatch($priceData, $forceUpdate); + } + + public static function bind(): void + { + app()->singleton(RetrievesPrice::class, static::class); + } +} diff --git a/src/Actions/Retrieval/SavePrice.php b/src/Actions/Retrieval/SavePrice.php new file mode 100644 index 0000000..22d71a4 --- /dev/null +++ b/src/Actions/Retrieval/SavePrice.php @@ -0,0 +1,43 @@ +firstOrNew([ + 'sku' => $priceData['sku'], + ]); + + $this->tierDuplicates->check($model, $priceData['tier_prices'] ?? []); + + $model->base_prices = $priceData['base_prices']; + $model->tier_prices = $priceData['tier_prices']; + $model->special_prices = $priceData['special_prices']; + + $model->sync = true; + $model->retrieve = false; + $model->last_retrieved = now(); + + $model->update = $forceUpdate || $model->checksum !== $priceData->checksum(); + $model->checksum = $priceData->checksum(); + + $model->save(); + } + + public static function bind(): void + { + app()->singleton(SavesPrice::class, static::class); + } +} diff --git a/src/Actions/SyncPrices.php b/src/Actions/SyncPrices.php deleted file mode 100644 index 3a9a6bc..0000000 --- a/src/Actions/SyncPrices.php +++ /dev/null @@ -1,40 +0,0 @@ -resetDoubleStatus(); - - MagentoPrice::shouldRetrieve() - ->select(['sku']) - ->take($retrieveLimit ?? config('magento-prices.retrieve_limit')) - ->get() - ->each(fn (MagentoPrice $price) => RetrievePriceJob::dispatch($price->sku)); - - MagentoPrice::shouldUpdate() - ->take($updateLimit ?? config('magento-prices.update_limit')) - ->get() - ->each(fn (MagentoPrice $price) => UpdatePriceJob::dispatch($price->sku)); - } - - protected function resetDoubleStatus(): void - { - MagentoPrice::query() - ->where('retrieve', '=', true) - ->where('update', '=', true) - ->update(['update' => false]); - } - - public static function bind(): void - { - app()->singleton(SyncsPrices::class, static::class); - } -} diff --git a/src/Actions/Update/Async/UpdateBasePricesAsync.php b/src/Actions/Update/Async/UpdateBasePricesAsync.php new file mode 100644 index 0000000..79c1278 --- /dev/null +++ b/src/Actions/Update/Async/UpdateBasePricesAsync.php @@ -0,0 +1,43 @@ +reject(fn (Price $price): bool => count($price->base_prices ?? []) === 0); + + if ($prices->isEmpty()) { + return; + } + + $payload = $prices + ->map(function (Price $price): array { + return [ + 'prices' => collect($price->base_prices) + ->map(fn (array $basePrice): array => array_merge($basePrice, [ + 'sku' => $price->sku, + ])) + ->toArray(), + ]; + }) + ->toArray(); + + $this->magentoAsync + ->subjects($prices->all()) + ->postBulk('products/base-prices', $payload); + } + + public static function bind(): void + { + app()->singleton(UpdatesBasePricesAsync::class, static::class); + } +} diff --git a/src/Actions/Update/Async/UpdatePricesAsync.php b/src/Actions/Update/Async/UpdatePricesAsync.php new file mode 100644 index 0000000..cb4b07c --- /dev/null +++ b/src/Actions/Update/Async/UpdatePricesAsync.php @@ -0,0 +1,33 @@ +basePrice->update($prices); + $this->tierPrice->update($prices); + $this->specialPrice->update($prices); + + $prices->each(fn (Price $price): bool => $price->update(['update' => false])); + } + + public static function bind(): void + { + app()->singleton(UpdatesPricesAsync::class, static::class); + } +} diff --git a/src/Actions/Update/Async/UpdateSpecialPricesAsync.php b/src/Actions/Update/Async/UpdateSpecialPricesAsync.php new file mode 100644 index 0000000..5bb03b0 --- /dev/null +++ b/src/Actions/Update/Async/UpdateSpecialPricesAsync.php @@ -0,0 +1,58 @@ +where('has_special', '=', true); + + foreach ($currentSpecialPrices as $price) { + $this->currentSpecialPrices->delete($price); + } + + $prices->each(fn (Price $price) => $price->update([ + 'has_special' => count($price->special_prices ?? []) > 0, + ])); + + $prices = $prices + ->reject(fn (Price $price): bool => count($price->special_prices ?? []) === 0); + + if ($prices->isEmpty()) { + return; + } + + $payload = $prices + ->map(function (Price $price): array { + return [ + 'prices' => collect($price->special_prices) + ->map(fn (array $tierPrice): array => array_merge($tierPrice, [ + 'sku' => $price->sku, + ])) + ->toArray(), + ]; + }) + ->toArray(); + + $this->magentoAsync + ->subjects($prices->all()) + ->postBulk('products/special-price', $payload); + } + + public static function bind(): void + { + app()->singleton(UpdatesSpecialPricesAsync::class, static::class); + } +} diff --git a/src/Actions/Update/Async/UpdateTierPricesAsync.php b/src/Actions/Update/Async/UpdateTierPricesAsync.php new file mode 100644 index 0000000..dd76557 --- /dev/null +++ b/src/Actions/Update/Async/UpdateTierPricesAsync.php @@ -0,0 +1,50 @@ +reject(fn (Price $price): bool => count($price->tier_prices ?? []) === 0); + + if ($prices->isEmpty()) { + return; + } + + $customerGroups = $this->customerGroups->retrieve(); + + $payload = $prices + ->map(function (Price $price) use ($customerGroups): array { + return [ + 'prices' => collect($price->tier_prices) + ->whereIn('customer_group', $customerGroups) + ->map(fn (array $tierPrice): array => array_merge($tierPrice, [ + 'sku' => $price->sku, + ])) + ->toArray(), + ]; + }) + ->toArray(); + + $this->magentoAsync + ->subjects($prices->all()) + ->putBulk('products/tier-prices', $payload); + } + + public static function bind(): void + { + app()->singleton(UpdatesTierPricesAsync::class, static::class); + } +} diff --git a/src/Actions/Update/Sync/UpdateBasePrice.php b/src/Actions/Update/Sync/UpdateBasePrice.php new file mode 100644 index 0000000..c8eb14b --- /dev/null +++ b/src/Actions/Update/Sync/UpdateBasePrice.php @@ -0,0 +1,45 @@ +base_prices) + ->map(fn (array $basePrice): array => array_merge($basePrice, [ + 'sku' => $price->sku, + ])); + + if ($payload->isEmpty()) { + return true; + } + + $response = $this->magento + ->post('products/base-prices', ['prices' => $payload]) + ->onError(function (Response $response) use ($price, $payload): void { + activity() + ->on($price) + ->useLog('error') + ->withProperties([ + 'response' => $response->body(), + 'payload' => $payload, + ]) + ->log('Failed to update base price'); + }); + + return $response->successful(); + } + + public static function bind(): void + { + app()->singleton(UpdatesBasePrice::class, static::class); + } +} diff --git a/src/Actions/Update/Sync/UpdatePrice.php b/src/Actions/Update/Sync/UpdatePrice.php new file mode 100644 index 0000000..bb0f3d6 --- /dev/null +++ b/src/Actions/Update/Sync/UpdatePrice.php @@ -0,0 +1,58 @@ +magentoExistence->exists($price->sku)) { + $price->update([ + 'update' => false, + ]); + + return; + } + + $results = []; + + $results[] = $this->basePrice->update($price); + $results[] = $this->tierPrice->update($price); + $results[] = $this->specialPrice->update($price); + + $hasFailure = in_array(false, $results); + + if ($hasFailure) { + $price->registerFailure(); + + return; + } + + $price->update([ + 'last_updated' => now(), + 'update' => false, + ]); + + event(new UpdatedPriceEvent($price)); + } + + public static function bind(): void + { + app()->singleton(UpdatesPrice::class, static::class); + } +} diff --git a/src/Actions/Update/Sync/UpdateSpecialPrice.php b/src/Actions/Update/Sync/UpdateSpecialPrice.php new file mode 100644 index 0000000..cafb705 --- /dev/null +++ b/src/Actions/Update/Sync/UpdateSpecialPrice.php @@ -0,0 +1,57 @@ +special_prices) + ->map(fn (array $specialPrice): array => array_merge($specialPrice, [ + 'sku' => $price->sku, + ])); + + if ($price->has_special) { + $this->currentSpecialPrices->delete($price); + } + + $price->update([ + 'has_special' => $payload->isNotEmpty(), + ]); + + if ($payload->isEmpty()) { + return true; + } + + $response = $this->magento + ->post('products/special-price', ['prices' => $payload]) + ->onError(function (Response $response) use ($price, $payload): void { + activity() + ->on($price) + ->useLog('error') + ->withProperties([ + 'response' => $response->body(), + 'payload' => $payload, + ]) + ->log('Failed to update special price'); + }); + + return $response->successful(); + } + + public static function bind(): void + { + app()->singleton(UpdatesSpecialPrice::class, static::class); + } +} diff --git a/src/Actions/Update/Sync/UpdateTierPrice.php b/src/Actions/Update/Sync/UpdateTierPrice.php new file mode 100644 index 0000000..ae246b5 --- /dev/null +++ b/src/Actions/Update/Sync/UpdateTierPrice.php @@ -0,0 +1,50 @@ +tier_prices) + ->whereIn('customer_group', $this->customerGroups->retrieve()) + ->map(fn (array $tierPrice): array => array_merge($tierPrice, [ + 'sku' => $price->sku, + ])); + + if ($payload->isEmpty()) { + return true; + } + + $response = $this->magento + ->post('products/tier-prices', ['prices' => $payload]) + ->onError(function (Response $response) use ($price, $payload): void { + activity() + ->on($price) + ->useLog('error') + ->withProperties([ + 'response' => $response->body(), + 'payload' => $payload, + ]) + ->log('Failed to update tier price'); + }); + + return $response->successful(); + } + + public static function bind(): void + { + app()->singleton(UpdatesTierPrice::class, static::class); + } +} diff --git a/src/Actions/UpdateMagentoBasePrice.php b/src/Actions/UpdateMagentoBasePrice.php deleted file mode 100644 index f0e3f01..0000000 --- a/src/Actions/UpdateMagentoBasePrice.php +++ /dev/null @@ -1,38 +0,0 @@ -magento->postAsync('products/base-prices', ['prices' => $priceData->getMagentoBasePrices()]); - } else { - $response = $this->magento->post('products/base-prices', ['prices' => $priceData->getMagentoBasePrices()]); - } - - $response->throw(); - - $model = $priceData->getModel(); - $model->update(['last_updated' => now()]); - - activity() - ->performedOn($model) - ->withProperties($response->json()) - ->log('Updated base price in Magento'); - } - - public static function bind(): void - { - app()->singleton(UpdatesMagentoBasePrice::class, static::class); - } -} diff --git a/src/Actions/UpdateMagentoSpecialPrices.php b/src/Actions/UpdateMagentoSpecialPrices.php deleted file mode 100644 index 86e4077..0000000 --- a/src/Actions/UpdateMagentoSpecialPrices.php +++ /dev/null @@ -1,55 +0,0 @@ -deleteCurrentSpecialPrices($priceData->sku); - - if (config('magento-prices.async')) { - $response = $this->magento->postAsync('products/special-price', ['prices' => $priceData->getMagentoSpecialPrices()]); - } else { - $response = $this->magento->post('products/special-price', ['prices' => $priceData->getMagentoSpecialPrices()]); - } - - $response->throw(); - - $model = $priceData->getModel(); - $model->update(['last_updated' => now()]); - - activity() - ->performedOn($model) - ->withProperties($response->json()) - ->log('Updated special price in Magento'); - } - - protected function deleteCurrentSpecialPrices(string $sku): void - { - $this->magento - ->post('products/special-price-information', ['skus' => [$sku]]) - ->throw() - ->collect() - ->chunk(20) - ->each(function (Enumerable $enumerable): void { - $this->magento - ->post('products/special-price-delete', ['prices' => $enumerable->toArray()]) - ->throw(); - }); - } - - public static function bind(): void - { - app()->singleton(UpdatesMagentoSpecialPrice::class, static::class); - } -} diff --git a/src/Actions/UpdateMagentoTierPrices.php b/src/Actions/UpdateMagentoTierPrices.php deleted file mode 100644 index 8231315..0000000 --- a/src/Actions/UpdateMagentoTierPrices.php +++ /dev/null @@ -1,66 +0,0 @@ -getGroups(); - - $tierPrices = collect($priceData->getMagentoTierPrices()) - ->whereIn('customer_group', $existingGroups); - - if (config('magento-prices.async')) { - $response = $this->magento->putAsync('products/tier-prices', ['prices' => $tierPrices->toArray()]); - } else { - $response = $this->magento->put('products/tier-prices', ['prices' => $tierPrices->toArray()]); - } - - $response->throw(); - - $model = $priceData->getModel(); - $model->update(['last_updated' => now()]); - - activity() - ->performedOn($model) - ->withProperties($response->json()) - ->log('Updated tier price in Magento'); - } - - protected function getGroups(): array - { - cache()->remember('magento:prices:customer:groups:imported', now()->addDay(), function (): bool { - ImportCustomerGroupsJob::dispatch(); - - return true; - }); - - $groups = MagentoCustomerGroup::query()->pluck('code'); - - if ($groups->isEmpty()) { - throw new PriceUpdateException('The Magento customer groups are not imported'); - } - - return $groups - ->push('ALL GROUPS') - ->toArray(); - } - - public static function bind(): void - { - app()->singleton(UpdatesMagentoTierPrice::class, static::class); - } -} diff --git a/src/Actions/Utility/CheckTierDuplicates.php b/src/Actions/Utility/CheckTierDuplicates.php new file mode 100644 index 0000000..1b23577 --- /dev/null +++ b/src/Actions/Utility/CheckTierDuplicates.php @@ -0,0 +1,43 @@ +groupBy(['website_id', 'quantity', 'customer_group']) + ->flatten(2) + ->filter(fn (Collection $matches): bool => $matches->count() > 1); + + if ($duplicates->isEmpty()) { + return; + } + + activity() + ->when($model->exists, fn (ActivityLogger $logger): ActivityLogger => $logger->on($model)) + ->useLog('error') + ->withProperties([ + 'duplicate' => $duplicates->toArray(), + ]) + ->log("Duplicate tier prices found for $model->sku"); + + throw new DuplicateTierPriceException("Duplicate tier prices found for $model->sku. Duplicates: ".json_encode($duplicates->toArray())); + } + + public static function bind(): void + { + app()->singleton(ChecksTierDuplicates::class, static::class); + } +} diff --git a/src/Actions/Utility/DeleteCurrentSpecialPrices.php b/src/Actions/Utility/DeleteCurrentSpecialPrices.php new file mode 100644 index 0000000..e5ba921 --- /dev/null +++ b/src/Actions/Utility/DeleteCurrentSpecialPrices.php @@ -0,0 +1,43 @@ +magento + ->post('products/special-price-information', ['skus' => [$price->sku]]) + ->throw() + ->collect() + ->chunk(20) + ->each(function (Enumerable $specialPrices) use ($price): void { + $this->magento + ->post('products/special-price-delete', ['prices' => $specialPrices->toArray()]) + ->onError(function (Response $response) use ($price, $specialPrices): void { + activity() + ->on($price) + ->useLog('error') + ->withProperties([ + 'response' => $response->body(), + 'payload' => $specialPrices->toArray(), + ]) + ->log('Failed to remove special price'); + }) + ->throw(); + }); + } + + public static function bind(): void + { + app()->singleton(DeletesCurrentSpecialPrices::class, static::class); + } +} diff --git a/src/Actions/ImportCustomerGroups.php b/src/Actions/Utility/ImportCustomerGroups.php similarity index 55% rename from src/Actions/ImportCustomerGroups.php rename to src/Actions/Utility/ImportCustomerGroups.php index 27b77a9..0beeec1 100644 --- a/src/Actions/ImportCustomerGroups.php +++ b/src/Actions/Utility/ImportCustomerGroups.php @@ -1,57 +1,57 @@ magento->lazy('customerGroups/search')->each(function (array $group) use ($date): void { - MagentoCustomerGroup::query()->updateOrCreate([ - 'code' => $group['code'], - ], [ - 'data' => $group, - 'imported_at' => $date, - ]); - }); - - $deletions = MagentoCustomerGroup::query() + $this->magento->lazy('customerGroups/search') + ->each(function (array $group) use ($date): void { + CustomerGroup::query()->updateOrCreate([ + 'code' => $group['code'], + ], [ + 'data' => $group, + 'imported_at' => $date, + ]); + }); + + $deletions = CustomerGroup::query() ->where('imported_at', '<', $date) ->orWhereNull('imported_at') ->get(); - $newGroupCount = MagentoCustomerGroup::query() + $newGroupCount = CustomerGroup::query() ->where('created_at', '>=', $date) ->count(); // If a new group has been added, tier prices may be missing. if ($newGroupCount > 0) { - MagentoPrice::query() + Price::query() ->where('sync', '=', true) ->update(['update' => true]); } // Previous updates may have failed if a group has been removed. if ($deletions->isNotEmpty()) { - MagentoPrice::query()->update([ + Price::query()->update([ 'sync' => true, 'update' => true, ]); } - $deletions->each(function (MagentoCustomerGroup $group): void { + $deletions->each(function (CustomerGroup $group): void { $group->delete(); }); } diff --git a/src/Actions/Utility/ProcessProductsWithMissingPrices.php b/src/Actions/Utility/ProcessProductsWithMissingPrices.php new file mode 100644 index 0000000..836d791 --- /dev/null +++ b/src/Actions/Utility/ProcessProductsWithMissingPrices.php @@ -0,0 +1,84 @@ +retrieveSkus(); + + /** @var bool $async */ + $async = config('magento-prices.async'); + + $pricesToUpdate = collect(); + + foreach ($skus as $sku) { + /** @var ?Price $price */ + $price = Price::query()->firstWhere('sku', '=', $sku); + + if ($price !== null) { + + if ($async) { + $pricesToUpdate[] = $price; + } else { + UpdatePriceJob::dispatch($price); + } + + continue; + } + + RetrievePriceJob::dispatch($sku); + } + + if ($async) { + $repository = BaseRepository::resolve(); + + $pricesToUpdate + ->chunk($repository->updateLimit()) + ->each(fn (Collection $chunk): PendingDispatch => UpdatePricesAsyncJob::dispatch($chunk)); + } + } + + /** @return LazyCollection */ + public function retrieveSkus(): LazyCollection + { + return LazyCollection::make(function () { + $searchCriteria = SearchCriteria::make() + ->select(['sku', 'price', 'type_id']) + ->get(); + + $products = $this->magento->lazy('products', $searchCriteria); + + foreach ($products as $product) { + if ( + (array_key_exists('price', $product) && floatval($product['price']) > 0) || + (array_key_exists('type_id', $product) && $product['type_id'] != 'simple') + ) { + continue; + } + + yield $product['sku']; + } + }); + } + + public static function bind(): void + { + app()->singleton(ProcessesProductsWithMissingPrices::class, static::class); + } +} diff --git a/src/Actions/Utility/RetrieveCustomerGroups.php b/src/Actions/Utility/RetrieveCustomerGroups.php new file mode 100644 index 0000000..bb58c1d --- /dev/null +++ b/src/Actions/Utility/RetrieveCustomerGroups.php @@ -0,0 +1,35 @@ +remember('magento:prices:customer:groups:imported', now()->addDay(), function (): bool { + ImportCustomerGroupsJob::dispatch(); + + return true; + }); + + $groups = CustomerGroup::query()->pluck('code'); + + if ($groups->isEmpty()) { + throw new PriceUpdateException('The Magento customer groups are not imported'); + } + + return $groups + ->push('ALL GROUPS') + ->toArray(); + } + + public static function bind(): void + { + app()->singleton(RetrievesCustomerGroups::class, static::class); + } +} diff --git a/src/Commands/MonitorWaitTimesCommand.php b/src/Commands/MonitorWaitTimesCommand.php deleted file mode 100644 index abc3c55..0000000 --- a/src/Commands/MonitorWaitTimesCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -info('Dispatching...'); - - MonitorWaitTimesJob::dispatch(); - - $this->info('Done!'); - - return static::SUCCESS; - } -} diff --git a/src/Commands/ProcessPricesCommand.php b/src/Commands/ProcessPricesCommand.php new file mode 100644 index 0000000..beeb1f6 --- /dev/null +++ b/src/Commands/ProcessPricesCommand.php @@ -0,0 +1,20 @@ +argument('from'); + + $carbon = blank($from) ? null : Carbon::parse($from); + + RetrieveAllPricesJob::dispatch($carbon); + + return static::SUCCESS; + } +} diff --git a/src/Commands/Retrieval/RetrievePriceCommand.php b/src/Commands/Retrieval/RetrievePriceCommand.php new file mode 100644 index 0000000..ee12925 --- /dev/null +++ b/src/Commands/Retrieval/RetrievePriceCommand.php @@ -0,0 +1,23 @@ +argument('sku'); + + RetrievePriceJob::dispatch($sku); + + return static::SUCCESS; + } +} diff --git a/src/Commands/RetrievePricesCommand.php b/src/Commands/RetrievePricesCommand.php deleted file mode 100644 index 1c5976c..0000000 --- a/src/Commands/RetrievePricesCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -info('Dispatching...'); - - if ($this->argument('sku') !== null) { - RetrievePriceJob::dispatch($this->argument('sku')); - } else { - RetrievePricesJob::dispatch($this->option('date') !== null ? Carbon::parse($this->option('date')) : null); - } - - $this->info('Done!'); - - return static::SUCCESS; - } -} diff --git a/src/Commands/SearchMissingPricesCommand.php b/src/Commands/SearchMissingPricesCommand.php deleted file mode 100644 index 5559879..0000000 --- a/src/Commands/SearchMissingPricesCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -info('Dispatching...'); - - SyncMissingPricesJob::dispatch(); - - $this->info('Done!'); - - return static::SUCCESS; - } -} diff --git a/src/Commands/SyncPricesCommand.php b/src/Commands/SyncPricesCommand.php deleted file mode 100644 index 0f1fd72..0000000 --- a/src/Commands/SyncPricesCommand.php +++ /dev/null @@ -1,26 +0,0 @@ -option('sync')) { - /** @phpstan-ignore-next-line */ - SyncPricesJob::dispatchSync($this->argument('limit'), $this->argument('limit')); - } else { - /** @phpstan-ignore-next-line */ - SyncPricesJob::dispatch($this->argument('limit'), $this->argument('limit')); - } - - return static::SUCCESS; - } -} diff --git a/src/Commands/Update/UpdateAllPricesCommand.php b/src/Commands/Update/UpdateAllPricesCommand.php new file mode 100644 index 0000000..36cad93 --- /dev/null +++ b/src/Commands/Update/UpdateAllPricesCommand.php @@ -0,0 +1,28 @@ +whereHas('product', function (Builder $query): void { + $query->where('exists_in_magento', '=', true); + }) + ->get() + ->each(fn (Price $price): PendingDispatch => UpdatePriceJob::dispatch($price)); + + return static::SUCCESS; + } +} diff --git a/src/Commands/Update/UpdatePriceCommand.php b/src/Commands/Update/UpdatePriceCommand.php new file mode 100644 index 0000000..8f16491 --- /dev/null +++ b/src/Commands/Update/UpdatePriceCommand.php @@ -0,0 +1,29 @@ +argument('sku'); + + /** @var Price $price */ + $price = Price::query() + ->where('sku', '=', $sku) + ->firstOrFail(); + + UpdatePriceJob::dispatch($price); + + return static::SUCCESS; + } +} diff --git a/src/Commands/UpdatePriceCommand.php b/src/Commands/UpdatePriceCommand.php deleted file mode 100644 index b5749db..0000000 --- a/src/Commands/UpdatePriceCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -info('Dispatching...'); - - /** @var string $sku */ - $sku = $this->argument('sku'); - - UpdatePriceJob::dispatch($sku); - - $this->info('Done!'); - - return static::SUCCESS; - } -} diff --git a/src/Commands/ImportCustomerGroupsCommand.php b/src/Commands/Utility/ImportCustomerGroupsCommand.php similarity index 60% rename from src/Commands/ImportCustomerGroupsCommand.php rename to src/Commands/Utility/ImportCustomerGroupsCommand.php index 63d3fbe..ccd3f94 100644 --- a/src/Commands/ImportCustomerGroupsCommand.php +++ b/src/Commands/Utility/ImportCustomerGroupsCommand.php @@ -1,13 +1,13 @@ rules())->validate(); + } + + public function validated(): array + { + return Validator::make($this->toArray(), $this->rules())->validated(); + } + + public function rules(): array + { + return $this->rules; + } +} diff --git a/src/Contracts/ChecksTierDuplicates.php b/src/Contracts/ChecksTierDuplicates.php deleted file mode 100644 index e28a287..0000000 --- a/src/Contracts/ChecksTierDuplicates.php +++ /dev/null @@ -1,10 +0,0 @@ - list of skus */ - public function retrieve(): Enumerable; -} diff --git a/src/Contracts/MonitorsWaitTimes.php b/src/Contracts/MonitorsWaitTimes.php deleted file mode 100644 index efd5c5f..0000000 --- a/src/Contracts/MonitorsWaitTimes.php +++ /dev/null @@ -1,8 +0,0 @@ - */ - public function retrieveAll(): Enumerable; - - /** @return Enumerable */ - public function retrieveByDate(Carbon $from): Enumerable; -} diff --git a/src/Contracts/SyncsPrices.php b/src/Contracts/SyncsPrices.php deleted file mode 100644 index 8c942e9..0000000 --- a/src/Contracts/SyncsPrices.php +++ /dev/null @@ -1,8 +0,0 @@ - $prices */ + public function update(Collection $prices): void; +} diff --git a/src/Contracts/Update/Async/UpdatesPricesAsync.php b/src/Contracts/Update/Async/UpdatesPricesAsync.php new file mode 100644 index 0000000..d86723e --- /dev/null +++ b/src/Contracts/Update/Async/UpdatesPricesAsync.php @@ -0,0 +1,12 @@ + $prices */ + public function update(Collection $prices): void; +} diff --git a/src/Contracts/Update/Async/UpdatesSpecialPricesAsync.php b/src/Contracts/Update/Async/UpdatesSpecialPricesAsync.php new file mode 100644 index 0000000..c36de89 --- /dev/null +++ b/src/Contracts/Update/Async/UpdatesSpecialPricesAsync.php @@ -0,0 +1,12 @@ + $prices */ + public function update(Collection $prices): void; +} diff --git a/src/Contracts/Update/Async/UpdatesTierPricesAsync.php b/src/Contracts/Update/Async/UpdatesTierPricesAsync.php new file mode 100644 index 0000000..75ee601 --- /dev/null +++ b/src/Contracts/Update/Async/UpdatesTierPricesAsync.php @@ -0,0 +1,12 @@ + $prices */ + public function update(Collection $prices): void; +} diff --git a/src/Contracts/Update/Sync/UpdatesBasePrice.php b/src/Contracts/Update/Sync/UpdatesBasePrice.php new file mode 100644 index 0000000..1fc0cfd --- /dev/null +++ b/src/Contracts/Update/Sync/UpdatesBasePrice.php @@ -0,0 +1,10 @@ +price = $price; - $this->storeId = $storeId; - } - - public function getStoreId(): int - { - return $this->storeId; - } - - public function setStoreId(int $storeId): void - { - $this->storeId = $storeId; - } - - public function getPrice(): Money - { - return $this->price; - } - - public function setPrice(Money $price): void - { - $this->price = $price; - } - - public function parsePrice(mixed $price): static - { - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - $this->price = $helper->getMoney($price); - - return $this; - } - - public function equals(self $other): bool - { - return $this->price->isEqualTo($other->price) && - $this->storeId === $other->storeId; - } - - public function toArray(): array - { - return [ - 'storeId' => $this->storeId, - 'price' => (string) $this->price->getAmount(), - ]; - } -} diff --git a/src/Data/Data.php b/src/Data/Data.php new file mode 100644 index 0000000..69e5667 --- /dev/null +++ b/src/Data/Data.php @@ -0,0 +1,48 @@ +validate($data); + } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->data); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->data[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->data[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->data[$offset]); + } + + public static function of(array $data): static + { + return new static($data); + } + + public function toArray(): array + { + return $this->data; + } +} diff --git a/src/Data/PriceData.php b/src/Data/PriceData.php index 67b5a2b..ebc9dc8 100644 --- a/src/Data/PriceData.php +++ b/src/Data/PriceData.php @@ -2,121 +2,35 @@ namespace JustBetter\MagentoPrices\Data; -use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Support\Collection; -use JustBetter\MagentoPrices\Actions\CheckTierDuplicates; -use JustBetter\MagentoPrices\Contracts\DeterminesPricesEqual; -use JustBetter\MagentoPrices\Models\MagentoPrice; - -class PriceData implements Arrayable +class PriceData extends Data { - public string $sku; - - /** @var Collection */ - public Collection $basePrices; - - /** @var Collection */ - public Collection $tierPrices; - - /** @var Collection */ - public Collection $specialPrices; - - public function __construct( - string $sku, - Collection $basePrices, - ?Collection $tierPrices = null, - ?Collection $specialPrices = null - ) { - $this->sku = $sku; - $this->basePrices = $basePrices; - $this->tierPrices = $tierPrices ?? collect(); - $this->specialPrices = $specialPrices ?? collect(); - } - - public function toArray(): array - { - return [ - 'base_prices' => $this->basePrices->map(fn (BasePriceData $data) => $data->toArray()), - 'tier_prices' => $this->tierPrices->map(fn (TierPriceData $data) => $data->toArray()), - 'special_prices' => $this->specialPrices->map(fn (SpecialPriceData $data) => $data->toArray()), - ]; - } - - public function getMagentoBasePrices(): array - { - $magentoPrices = []; - - /** @var BasePriceData $basePrice */ - foreach ($this->basePrices as $basePrice) { - $magentoPrices[] = [ - 'sku' => $this->sku, - 'price' => $basePrice->getPrice()->getAmount()->toFloat(), - 'store_id' => $basePrice->getStoreId(), - ]; - } - - return $magentoPrices; - } - - public function getMagentoTierPrices(): array - { - $magentoPrices = []; - - /** @var TierPriceData $tierPrice */ - foreach ($this->tierPrices as $tierPrice) { - $magentoPrices[] = [ - 'sku' => $this->sku, - 'price' => $tierPrice->getPrice()->getAmount()->toFloat(), - 'website_id' => $tierPrice->getStoreId(), - 'quantity' => $tierPrice->getQuantity() < 1 ? 1 : $tierPrice->getQuantity(), - 'customer_group' => $tierPrice->getGroupId(), - 'price_type' => $tierPrice->getPriceType(), - ]; - } - - return $magentoPrices; - } - - public function getMagentoSpecialPrices(): array - { - $magentoPrices = []; - - /** @var SpecialPriceData $specialPrice */ - foreach ($this->specialPrices as $specialPrice) { - $magentoPrices[] = [ - 'sku' => $this->sku, - 'price' => $specialPrice->getPrice()->getAmount()->toFloat(), - 'store_id' => $specialPrice->getStoreId(), - 'price_from' => $specialPrice->getFrom()->format('Y-m-d H:i:s'), - 'price_to' => $specialPrice->getTo()->format('Y-m-d H:i:s'), - ]; - } - - return $magentoPrices; - } - - public function getModel(): MagentoPrice - { - /** @var MagentoPrice $price */ - $price = MagentoPrice::query() - ->firstOrCreate(['sku' => $this->sku]); - - return $price; - } - - public function equals(self $other): bool + public array $rules = [ + 'sku' => ['required', 'max:255'], + + 'base_prices' => ['nullable', 'array'], + 'base_prices.*.store_id' => ['required', 'integer'], + 'base_prices.*.price' => ['required', 'numeric'], + + 'tier_prices' => ['nullable', 'array'], + 'tier_prices.*.website_id' => ['required', 'integer'], + 'tier_prices.*.customer_group' => ['required', 'string'], + 'tier_prices.*.price_type' => ['required', 'string'], + 'tier_prices.*.quantity' => ['required', 'numeric'], + 'tier_prices.*.price' => ['required', 'numeric'], + + 'special_prices' => ['nullable', 'array'], + 'special_prices.*.store_id' => ['required', 'integer'], + 'special_prices.*.price' => ['required', 'numeric'], + 'special_prices.*.price_from' => ['required', 'string'], + 'special_prices.*.price_to' => ['required', 'string'], + ]; + + public function checksum(): string { - /** @var DeterminesPricesEqual $check */ - $check = app(DeterminesPricesEqual::class); - - return $check->equals($this, $other); - } + $json = json_encode($this->validated()); - public function validate(): void - { - /** @var CheckTierDuplicates $tierDuplicates */ - $tierDuplicates = app(CheckTierDuplicates::class); + throw_if($json === false, 'Failed to generate checksum'); - $tierDuplicates->check($this->sku, $this->tierPrices); + return md5($json); } } diff --git a/src/Data/SpecialPriceData.php b/src/Data/SpecialPriceData.php deleted file mode 100644 index 1333825..0000000 --- a/src/Data/SpecialPriceData.php +++ /dev/null @@ -1,99 +0,0 @@ -price = $price; - $this->storeId = $storeId; - $this->from = $from ?? Carbon::createFromTimestamp(0); - $this->to = $to ?? Carbon::createFromTimestamp(2147483647); - } - - public function getStoreId(): int - { - return $this->storeId; - } - - public function setStoreId(int $storeId): void - { - $this->storeId = $storeId; - } - - public function getPrice(): Money - { - return $this->price; - } - - public function setPrice(Money $price): void - { - $this->price = $price; - } - - public function getFrom(): Carbon - { - return $this->from; - } - - public function setFrom(Carbon $from): void - { - $this->from = $from; - } - - public function getTo(): Carbon - { - return $this->to; - } - - public function setTo(Carbon $to): void - { - $this->to = $to; - } - - public function parsePrice(mixed $price): self - { - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - $this->price = $helper->getMoney($price); - - return $this; - } - - public function equals(self $other): bool - { - if (! $this->price->isEqualTo($other->price)) { - return false; - } - - if (! $this->from->startOfDay()->equalTo($other->from->startOfDay()) || ! $this->to->startOfDay()->equalTo($other->to->startOfDay())) { - return false; - } - - return true; - } - - public function toArray(): array - { - return [ - 'storeId' => $this->storeId, - 'price' => (string) $this->price->getAmount(), - 'price_from' => $this->from->format('Y-m-d H:i:s'), - 'price_to' => $this->to->format('Y-m-d H:i:s'), - ]; - } -} diff --git a/src/Data/TierPriceData.php b/src/Data/TierPriceData.php deleted file mode 100644 index 666406f..0000000 --- a/src/Data/TierPriceData.php +++ /dev/null @@ -1,107 +0,0 @@ -groupId = $groupId; - $this->price = $price; - $this->quantity = $quantity; - $this->storeId = $storeId; - $this->priceType = $priceType; - } - - public function getPriceType(): string - { - return $this->priceType; - } - - public function setPriceType(string $priceType): void - { - $this->priceType = $priceType; - } - - public function getStoreId(): int - { - return $this->storeId; - } - - public function setStoreId(int $storeId): void - { - $this->storeId = $storeId; - } - - public function getQuantity(): int - { - return $this->quantity; - } - - public function setQuantity(int $quantity): void - { - $this->quantity = $quantity; - } - - public function getPrice(): Money - { - return $this->price; - } - - public function setPrice(Money $price): void - { - $this->price = $price; - } - - public function getGroupId(): string - { - return $this->groupId; - } - - public function setGroupId(string $groupId): void - { - $this->groupId = $groupId; - } - - /** Get unique identifier for this store, qty and group */ - public function getIdentifier(): string - { - return implode('-', [$this->storeId, $this->quantity, $this->groupId]); - } - - public function equals(self $other): bool - { - return $this->price->isEqualTo($other->price) && - $this->groupId === $other->groupId && - $this->storeId === $other->storeId && - $this->priceType === $other->priceType; - } - - public function toArray(): array - { - return [ - 'storeId' => $this->storeId, - 'quantity' => $this->quantity, - 'customer_group' => $this->groupId, - 'price' => (string) $this->price->getAmount(), - ]; - } -} diff --git a/src/Events/LongWaitDetectedEvent.php b/src/Events/LongWaitDetectedEvent.php deleted file mode 100644 index 048a7d9..0000000 --- a/src/Events/LongWaitDetectedEvent.php +++ /dev/null @@ -1,14 +0,0 @@ -storeId, Group: $tier->groupId, Qty: $tier->quantity.".PHP_EOL; - } - - parent::__construct($message); - } -} +class DuplicateTierPriceException extends Exception {} diff --git a/src/Exceptions/NotSupportedException.php b/src/Exceptions/NotImplementedException.php similarity index 58% rename from src/Exceptions/NotSupportedException.php rename to src/Exceptions/NotImplementedException.php index badea83..f273a49 100644 --- a/src/Exceptions/NotSupportedException.php +++ b/src/Exceptions/NotImplementedException.php @@ -4,6 +4,4 @@ use Exception; -class NotSupportedException extends Exception -{ -} +class NotImplementedException extends Exception {} diff --git a/src/Exceptions/PriceUpdateException.php b/src/Exceptions/PriceUpdateException.php index fa5aad1..00d11e7 100644 --- a/src/Exceptions/PriceUpdateException.php +++ b/src/Exceptions/PriceUpdateException.php @@ -4,6 +4,4 @@ use Exception; -class PriceUpdateException extends Exception -{ -} +class PriceUpdateException extends Exception {} diff --git a/src/Helpers/MoneyHelper.php b/src/Helpers/MoneyHelper.php deleted file mode 100644 index 8d1f409..0000000 --- a/src/Helpers/MoneyHelper.php +++ /dev/null @@ -1,19 +0,0 @@ -onQueue(config('magento-prices.queue')); } - public function handle(MonitorsWaitTimes $waitTimes): void + public function handle(ProcessesPrices $prices): void { - $waitTimes->monitor(); + $prices->process(); } } diff --git a/src/Jobs/Retrieval/RetrieveAllPricesJob.php b/src/Jobs/Retrieval/RetrieveAllPricesJob.php new file mode 100644 index 0000000..baf48b9 --- /dev/null +++ b/src/Jobs/Retrieval/RetrieveAllPricesJob.php @@ -0,0 +1,28 @@ +onQueue(config('magento-prices.queue')); + } + + public function handle(RetrievesAllPrices $prices): void + { + $prices->retrieve($this->from); + } +} diff --git a/src/Jobs/Retrieval/RetrievePriceJob.php b/src/Jobs/Retrieval/RetrievePriceJob.php new file mode 100644 index 0000000..8150768 --- /dev/null +++ b/src/Jobs/Retrieval/RetrievePriceJob.php @@ -0,0 +1,58 @@ +onQueue(config('magento-prices.queue')); + } + + public function handle(RetrievesPrice $price): void + { + $price->retrieve($this->sku, $this->forceUpdate); + } + + public function uniqueId(): string + { + return $this->sku; + } + + public function tags(): array + { + return [ + $this->sku, + ]; + } + + /** @codeCoverageIgnore */ + public function failed(Throwable $exception): void + { + /** @var ?Price $model */ + $model = Price::query()->firstWhere('sku', '=', $this->sku); + + activity() + ->when($model, function (ActivityLogger $logger, Price $price): ActivityLogger { + return $logger->on($price); + }) + ->useLog('error') + ->log('Failed to retrieve price: '.$exception->getMessage()); + } +} diff --git a/src/Jobs/Retrieval/SavePriceJob.php b/src/Jobs/Retrieval/SavePriceJob.php new file mode 100644 index 0000000..e5f6d01 --- /dev/null +++ b/src/Jobs/Retrieval/SavePriceJob.php @@ -0,0 +1,59 @@ +onQueue(config('magento-prices.queue')); + } + + public function handle(SavesPrice $price): void + { + $price->save($this->data, $this->forceUpdate); + } + + public function uniqueId(): string + { + return $this->data['sku']; + } + + public function tags(): array + { + return [ + $this->data['sku'], + ]; + } + + /** @codeCoverageIgnore */ + public function failed(Throwable $exception): void + { + /** @var ?Price $model */ + $model = Price::query()->firstWhere('sku', '=', $this->data['sku']); + + activity() + ->when($model, function (ActivityLogger $logger, Price $price): ActivityLogger { + return $logger->on($price); + }) + ->useLog('error') + ->log('Failed to save price: '.$exception->getMessage()); + } +} diff --git a/src/Jobs/RetrievePriceJob.php b/src/Jobs/RetrievePriceJob.php deleted file mode 100644 index cd9bae5..0000000 --- a/src/Jobs/RetrievePriceJob.php +++ /dev/null @@ -1,55 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(): void - { - /** @var RetrievesPrice $retriever */ - $retriever = app(config('magento-prices.retrievers.price')); - - $price = $retriever->retrieve($this->sku); - - if ($price === null) { - return; - } - - ProcessPriceJob::dispatch($price, $this->forceUpdate); - } - - public function uniqueId(): string - { - return $this->sku; - } - - public function tags(): array - { - return [ - $this->sku, - 'force:'.($this->forceUpdate ? 'true' : 'false'), - ]; - } -} diff --git a/src/Jobs/RetrievePricesJob.php b/src/Jobs/RetrievePricesJob.php deleted file mode 100644 index 4c48a17..0000000 --- a/src/Jobs/RetrievePricesJob.php +++ /dev/null @@ -1,54 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(): void - { - /** @var RetrievesSkus $retriever */ - $retriever = app(config('magento-prices.retrievers.sku')); - - $prices = $this->hasFromDate() - ? $retriever->retrieveByDate($this->from) - : $retriever->retrieveAll(); - - $prices->each(fn (string $sku) => RetrievePriceJob::dispatch($sku)); - } - - protected function hasFromDate(): bool - { - return $this->from !== null && is_a($this->from, DateTime::class); - } - - public function tags(): array - { - return [ - 'force:'.($this->forceUpdate ? 'true' : 'false'), - $this->hasFromDate() ? 'from:'.$this->from->toDateTimeString() : 'all', - ]; - } -} diff --git a/src/Jobs/SyncMissingPricesJob.php b/src/Jobs/SyncMissingPricesJob.php deleted file mode 100644 index ae200f8..0000000 --- a/src/Jobs/SyncMissingPricesJob.php +++ /dev/null @@ -1,46 +0,0 @@ -queue = config('magento-prices.queue'); - } - - public function handle(FindsProductsWithMissingPrices $findsProductsWithMissingPrices): void - { - $products = $findsProductsWithMissingPrices->retrieve(); - - foreach ($products as $sku) { - $price = MagentoPrice::findBySku($sku); - - if ($price !== null) { - UpdatePriceJob::dispatch($sku); - - continue; - } - - RetrievePriceJob::dispatch($sku); - } - } -} diff --git a/src/Jobs/SyncPricesJob.php b/src/Jobs/SyncPricesJob.php deleted file mode 100644 index 7349b50..0000000 --- a/src/Jobs/SyncPricesJob.php +++ /dev/null @@ -1,29 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(SyncsPrices $syncsPrices): void - { - $syncsPrices->sync($this->retrieveLimit, $this->updateLimit); - } -} diff --git a/src/Jobs/ProcessPriceJob.php b/src/Jobs/Update/UpdatePriceJob.php similarity index 52% rename from src/Jobs/ProcessPriceJob.php rename to src/Jobs/Update/UpdatePriceJob.php index b0ebe09..fbbe934 100644 --- a/src/Jobs/ProcessPriceJob.php +++ b/src/Jobs/Update/UpdatePriceJob.php @@ -1,6 +1,6 @@ onQueue(config('magento-prices.queue')); } - public function handle(ProcessesPrice $processesPrice): void + public function handle(UpdatesPrice $contract): void { - $processesPrice->process($this->price, $this->forceUpdate); + $contract->update($this->price); } - public function uniqueId(): string + public function uniqueId(): int { - return $this->price->sku; + return $this->price->id; } public function tags(): array diff --git a/src/Jobs/Update/UpdatePricesAsyncJob.php b/src/Jobs/Update/UpdatePricesAsyncJob.php new file mode 100644 index 0000000..c9ceda2 --- /dev/null +++ b/src/Jobs/Update/UpdatePricesAsyncJob.php @@ -0,0 +1,36 @@ + $prices */ + public function __construct(public Collection $prices) + { + $this->onQueue(config('magento-prices.queue')); + } + + public function handle(UpdatesPricesAsync $contract): void + { + $contract->update($this->prices); + } + + public function tags(): array + { + return $this->prices->pluck('sku')->toArray(); + } +} diff --git a/src/Jobs/UpdateMagentoBasePricesJob.php b/src/Jobs/UpdateMagentoBasePricesJob.php deleted file mode 100644 index ae04eef..0000000 --- a/src/Jobs/UpdateMagentoBasePricesJob.php +++ /dev/null @@ -1,69 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(UpdatesMagentoBasePrice $magentoBasePrice): void - { - $magentoBasePrice->update($this->price); - } - - public function failed(Throwable $exception): void - { - if (is_a($exception, RequestException::class)) { - $response = $exception->response->body(); - } - - activity() - ->on($this->price->getModel()) - ->useLog('error') - ->withProperties([ - 'priceData' => $this->price->getMagentoBasePrices(), - 'message' => $exception->getMessage(), - 'response' => $response ?? '', - ]) - ->log('Failed to update base prices'); - - $this->price->getModel()->registerError(); - } - - public function uniqueId(): string - { - return $this->price->sku; - } - - public function tags(): array - { - return [ - $this->price->sku, - ]; - } -} diff --git a/src/Jobs/UpdateMagentoSpecialPricesJob.php b/src/Jobs/UpdateMagentoSpecialPricesJob.php deleted file mode 100644 index bd305e6..0000000 --- a/src/Jobs/UpdateMagentoSpecialPricesJob.php +++ /dev/null @@ -1,69 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(UpdatesMagentoSpecialPrice $updatesMagentoSpecialPrice): void - { - $updatesMagentoSpecialPrice->update($this->price); - } - - public function failed(Throwable $exception): void - { - if (is_a($exception, RequestException::class)) { - $response = $exception->response->body(); - } - - activity() - ->on($this->price->getModel()) - ->useLog('error') - ->withProperties([ - 'priceData' => $this->price->getMagentoBasePrices(), - 'message' => $exception->getMessage(), - 'response' => $response ?? '', - ]) - ->log('Failed to update special prices'); - - $this->price->getModel()->registerError(); - } - - public function uniqueId(): string - { - return $this->price->sku; - } - - public function tags(): array - { - return [ - $this->price->sku, - ]; - } -} diff --git a/src/Jobs/UpdateMagentoTierPricesJob.php b/src/Jobs/UpdateMagentoTierPricesJob.php deleted file mode 100644 index e6615f7..0000000 --- a/src/Jobs/UpdateMagentoTierPricesJob.php +++ /dev/null @@ -1,69 +0,0 @@ -onQueue(config('magento-prices.queue')); - } - - public function handle(UpdatesMagentoTierPrice $updatesMagentoTierPrice): void - { - $updatesMagentoTierPrice->update($this->price); - } - - public function failed(Throwable $exception): void - { - if (is_a($exception, RequestException::class)) { - $response = $exception->response->body(); - } - - activity() - ->on($this->price->getModel()) - ->useLog('error') - ->withProperties([ - 'priceData' => $this->price->getMagentoBasePrices(), - 'message' => $exception->getMessage(), - 'response' => $response ?? '', - ]) - ->log('Failed to update tier prices'); - - $this->price->getModel()->registerError(); - } - - public function uniqueId(): string - { - return $this->price->sku; - } - - public function tags(): array - { - return [ - $this->price->sku, - ]; - } -} diff --git a/src/Jobs/UpdatePriceJob.php b/src/Jobs/UpdatePriceJob.php deleted file mode 100644 index b9a5689..0000000 --- a/src/Jobs/UpdatePriceJob.php +++ /dev/null @@ -1,142 +0,0 @@ -queue = config('magento-prices.queue'); - } - - public function handle(ChecksMagentoExistence $checksMagentoExistence): void - { - $model = MagentoPrice::findBySku($this->sku); - - if ($model === null) { - return; - } - - if (! $checksMagentoExistence->exists($model->sku)) { - $model->update([ - 'update' => false, - 'sync' => false, - ]); - - return; - } - - $data = $model->getData(); - - Bus::batch(array_filter([ - $this->handleBasePrices($data), - $this->handleTierPrices($data), - $this->handleSpecialPrices($data), - ])) - ->name("Magento price update $this->sku") - ->onQueue(config('magento-prices.queue')) - ->then(function () use ($model): void { - $model->update([ - 'fail_count' => 0, - 'last_failed' => null, - ]); - }) - ->dispatch(); - - $model->update([ - 'update' => false, - ]); - - UpdatedPriceEvent::dispatch($this->sku); - } - - protected function handleBasePrices(PriceData $data): ?UpdateMagentoBasePricesJob - { - if ($this->shouldUpdateType('base') && $data->basePrices->isNotEmpty()) { - return new UpdateMagentoBasePricesJob($data); - } - - return null; - } - - protected function handleTierPrices(PriceData $data): ?UpdateMagentoTierPricesJob - { - if ($this->shouldUpdateType('tier') && $data->tierPrices->isNotEmpty()) { - $data->getModel()->update([ - 'has_tier' => true, - ]); - - return new UpdateMagentoTierPricesJob($data); - } - - // Delete the tier price - if ($data->getModel()->has_tier) { - $data->getModel()->update([ - 'has_tier' => false, - ]); - - return new UpdateMagentoTierPricesJob($data); - } - - return null; - } - - protected function handleSpecialPrices(PriceData $data): ?UpdateMagentoSpecialPricesJob - { - if ($this->shouldUpdateType('special') && $data->specialPrices->isNotEmpty()) { - $data->getModel()->update([ - 'has_special' => true, - ]); - - return new UpdateMagentoSpecialPricesJob($data); - } - - // Delete the special price - if ($data->getModel()->has_special) { - $data->getModel()->update([ - 'has_special' => false, - ]); - - return new UpdateMagentoSpecialPricesJob($data); - } - - return null; - } - - protected function shouldUpdateType(string $type): bool - { - return blank($this->type) || $type == $this->type; - } - - public function uniqueId(): string - { - return $this->sku; - } - - public function tags(): array - { - return [ - $this->sku, - 'type:'.($this->type ?? 'all'), - ]; - } -} diff --git a/src/Jobs/ImportCustomerGroupsJob.php b/src/Jobs/Utility/ImportCustomerGroupsJob.php similarity index 82% rename from src/Jobs/ImportCustomerGroupsJob.php rename to src/Jobs/Utility/ImportCustomerGroupsJob.php index 544ab48..9b8b9b9 100644 --- a/src/Jobs/ImportCustomerGroupsJob.php +++ b/src/Jobs/Utility/ImportCustomerGroupsJob.php @@ -1,13 +1,13 @@ onQueue(config('magento-prices.queue')); + } + + public function handle(ProcessesProductsWithMissingPrices $contract): void + { + $contract->process(); + } +} diff --git a/src/Listeners/BulkOperationStatusListener.php b/src/Listeners/BulkOperationStatusListener.php new file mode 100644 index 0000000..3ffd3e3 --- /dev/null +++ b/src/Listeners/BulkOperationStatusListener.php @@ -0,0 +1,36 @@ +subject; + + if ($operation->status === OperationStatus::Complete) { + $price->update(['last_updated' => now()]); + + event(new UpdatedPriceEvent($price)); + + return; + } + + activity() + ->useLog('error') + ->withProperties([ + 'status' => $operation->status?->name ?? 'unknown', + 'response' => $operation->response, + ]) + ->log('Failed to update price'); + } +} diff --git a/src/Models/MagentoCustomerGroup.php b/src/Models/CustomerGroup.php similarity index 92% rename from src/Models/MagentoCustomerGroup.php rename to src/Models/CustomerGroup.php index abf8de2..df568e1 100644 --- a/src/Models/MagentoCustomerGroup.php +++ b/src/Models/CustomerGroup.php @@ -13,7 +13,7 @@ * @property ?Carbon $created_at * @property ?Carbon $updated_at */ -class MagentoCustomerGroup extends Model +class CustomerGroup extends Model { protected $table = 'magento_prices_customer_groups'; diff --git a/src/Models/MagentoPrice.php b/src/Models/MagentoPrice.php deleted file mode 100644 index a1bfb4e..0000000 --- a/src/Models/MagentoPrice.php +++ /dev/null @@ -1,226 +0,0 @@ - 'datetime', - 'last_updated' => 'datetime', - 'last_failed' => 'datetime', - 'base_prices' => 'array', - 'tier_prices' => 'array', - 'special_prices' => 'array', - 'sync' => 'boolean', - 'has_tier' => 'boolean', - 'has_special' => 'boolean', - 'retrieve' => 'boolean', - 'update' => 'boolean', - ]; - - protected $guarded = []; - - public function scopeShouldRetrieve(Builder $builder): Builder - { - return $builder - ->where('sync', true) - ->where('retrieve', true); - } - - public function scopeShouldUpdate(Builder $builder): Builder - { - return $builder - ->where('sync', true) - ->where('update', true); - } - - public function getBasePricesAttribute(?string $value): Collection - { - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - - $prices = json_decode($value, true) ?? []; - - if (array_key_exists('price', $prices)) { - $prices = [$prices]; - } - - return collect($prices) - ->map(fn ($p) => new BasePriceData( - $helper->getMoney($p['price']), - $p['storeId'] - )); - } - - public function getTierPricesAttribute(?string $value): Collection - { - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - - $prices = json_decode($value, true) ?? []; - - if (array_key_exists('price', $prices)) { - $prices = [$prices]; - } - - return collect($prices)->map(fn ($p) => new TierPriceData( - $p['customer_group'], - $helper->getMoney($p['price']), - $p['quantity'], - $p['storeId'] ?? 0, - $p['priceType'] ?? 'fixed', - )); - } - - public function getSpecialPricesAttribute(?string $value): Collection - { - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - - $prices = json_decode($value, true) ?? []; - - if (array_key_exists('price', $prices)) { - $prices = [$prices]; - } - - return collect($prices)->map(function ($p) use ($helper) { - $from = blank($p['price_from']) - ? now() - : Carbon::createFromFormat('Y-m-d H:i:s', $p['price_from']); - - $to = blank($p['price_to']) - ? now() - : Carbon::createFromFormat('Y-m-d H:i:s', $p['price_to']); - - return new SpecialPriceData( - $helper->getMoney($p['price']), - $p['storeId'], - $from, - $to - ); - }); - } - - public function getData(): PriceData - { - return new PriceData( - $this->sku, - $this->base_prices, - $this->tier_prices, - $this->special_prices - ); - } - - public function registerError(): void - { - $this->fail_count++; - $this->last_failed = now(); - - if ($this->fail_count > config('magento-prices.fail_count', 5)) { - $this->update = false; - $this->retrieve = false; - $this->fail_count = 0; - } - - $this->save(); - } - - public static function findBySku(string $sku): ?static - { - /** @var ?static $result */ - $result = static::query() - ->where('sku', $sku) - ->first(); - - return $result; - } - - public function specialPriceChanged(): bool - { - if (! $this->isDirty('special_prices')) { - return false; - } - - $original = collect($this->getOriginal('special_prices')); - $updated = $this->getAttribute('special_prices'); - - if (count($updated) === 0 && $original->isNotEmpty()) { - return true; - } - - /** @var SpecialPriceData $updatedSpecialPrice */ - foreach ($updated as $updatedSpecialPrice) { - /** @var ?SpecialPriceData $originalPrice */ - $originalPrice = $original->where('storeId', $updatedSpecialPrice->storeId)->first(); - - // Check if a new special price was added - if ($originalPrice === null) { - return true; - } - - // Check if price still is in the date range - $dateValid = $originalPrice->from->lessThan(now()) && $originalPrice->to->greaterThan(now()); - - if (! $dateValid) { - return true; - } - - // Check if the price has changed - if ($originalPrice->price->compareTo($updatedSpecialPrice->price) !== 0) { - return true; - } - } - - return false; - } - - public function activity(): MorphMany - { - return $this->morphMany( - ActivitylogServiceProvider::determineActivityModel(), - 'subject' - ); - } - - public function getActivitylogOptions(): LogOptions - { - return LogOptions::defaults() - ->logOnlyDirty() - ->dontSubmitEmptyLogs() - ->logOnly(['sync', 'base_prices', 'tier_prices', 'special_prices']); - } -} diff --git a/src/Models/Price.php b/src/Models/Price.php new file mode 100644 index 0000000..66babdb --- /dev/null +++ b/src/Models/Price.php @@ -0,0 +1,97 @@ + 'datetime', + 'last_updated' => 'datetime', + 'last_failed' => 'datetime', + 'base_prices' => 'array', + 'tier_prices' => 'array', + 'special_prices' => 'array', + 'sync' => 'boolean', + 'has_special' => 'boolean', + 'retrieve' => 'boolean', + 'update' => 'boolean', + ]; + + protected $guarded = []; + + public static function booted(): void + { + static::updating(function (self $model) { + if ($model->update && $model->retrieve) { + if (! $model->isDirty(['retrieve'])) { + $model->retrieve = false; + } else { + $model->update = false; + } + } + }); + } + + public function product(): HasOne + { + return $this->hasOne(MagentoProduct::class, 'sku', 'sku'); + } + + public function registerFailure(): void + { + $this->fail_count++; + $this->last_failed = now(); + + $shouldRetry = $this->fail_count < BaseRepository::resolve()->failLimit(); + $this->sync = $shouldRetry; + + if (! $shouldRetry) { + $this->update = false; + $this->retrieve = false; + $this->fail_count = 0; + } + + $this->save(); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->logOnly(['sync', 'base_prices', 'tier_prices', 'special_prices']); + } +} diff --git a/src/Repository/BaseRepository.php b/src/Repository/BaseRepository.php new file mode 100644 index 0000000..b4c200b --- /dev/null +++ b/src/Repository/BaseRepository.php @@ -0,0 +1,49 @@ +retrieveLimit; + } + + public function updateLimit(): int + { + return $this->updateLimit; + } + + public function failLimit(): int + { + return $this->failLimit; + } + + public static function resolve(): BaseRepository + { + /** @var ?class-string $repository */ + $repository = config('magento-prices.repository'); + + throw_if($repository === null, 'Repository has not been found.'); + + /** @var BaseRepository $instance */ + $instance = app($repository); + + return $instance; + } + + /** @return Enumerable */ + abstract public function skus(?Carbon $from = null): Enumerable; + + abstract public function retrieve(string $sku): ?PriceData; +} diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php new file mode 100644 index 0000000..d6f42fe --- /dev/null +++ b/src/Repository/Repository.php @@ -0,0 +1,27 @@ + $skus */ + $skus = MagentoProduct::query() + ->where('exists_in_magento', '=', true) + ->pluck('sku'); + + return $skus; + } +} diff --git a/src/Retriever/DummyPriceRetriever.php b/src/Retriever/DummyPriceRetriever.php deleted file mode 100644 index 7a17e0b..0000000 --- a/src/Retriever/DummyPriceRetriever.php +++ /dev/null @@ -1,20 +0,0 @@ -registerConfig() + ->registerActions(); + } + + protected function registerConfig(): static { $this->mergeConfigFrom(__DIR__.'/../config/magento-prices.php', 'magento-prices'); - $this->registerActions(); + return $this; } protected function registerActions(): static { - UpdateMagentoBasePrice::bind(); - UpdateMagentoSpecialPrices::bind(); - UpdateMagentoTierPrices::bind(); + RetrieveAllPrices::bind(); + RetrievePrice::bind(); + SavePrice::bind(); - FindProductsWithMissingPrices::bind(); - CheckTierDuplicates::bind(); + UpdatePricesAsync::bind(); + UpdateBasePricesAsync::bind(); + UpdateTierPricesAsync::bind(); + UpdateSpecialPricesAsync::bind(); - SyncPrices::bind(); - ProcessPrice::bind(); + UpdatePrice::bind(); + UpdateBasePrice::bind(); + UpdateTierPrice::bind(); + UpdateSpecialPrice::bind(); - MonitorWaitTimes::bind(); - DeterminePricesEqual::bind(); + ProcessPrices::bind(); + DeleteCurrentSpecialPrices::bind(); + RetrieveCustomerGroups::bind(); + CheckTierDuplicates::bind(); + ProcessProductsWithMissingPrices::bind(); ImportCustomerGroups::bind(); return $this; @@ -57,7 +79,7 @@ public function boot(): void ->bootCommands(); } - protected function bootConfig(): self + protected function bootConfig(): static { if ($this->app->runningInConsole()) { $this->publishes([ @@ -68,23 +90,24 @@ protected function bootConfig(): self return $this; } - protected function bootCommands(): self + protected function bootCommands(): static { if ($this->app->runningInConsole()) { $this->commands([ - ImportCustomerGroupsCommand::class, - RetrievePricesCommand::class, - SyncPricesCommand::class, + RetrieveAllPricesCommand::class, + RetrievePriceCommand::class, + UpdateAllPricesCommand::class, UpdatePriceCommand::class, - SearchMissingPricesCommand::class, - MonitorWaitTimesCommand::class, + ProcessPricesCommand::class, + ImportCustomerGroupsCommand::class, + ProcessProductsWithMissingPricesCommand::class, ]); } return $this; } - protected function bootMigrations(): self + protected function bootMigrations(): static { $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); diff --git a/tests/Actions/CheckTierDuplicatesTest.php b/tests/Actions/CheckTierDuplicatesTest.php deleted file mode 100644 index a310907..0000000 --- a/tests/Actions/CheckTierDuplicatesTest.php +++ /dev/null @@ -1,51 +0,0 @@ -check('::sku::', $prices); - } catch (DuplicateTierPriceException $e) { - $this->assertTrue(false, 'exception thrown'); - } - - $this->assertTrue(true); - } - - public function test_it_fails(): void - { - $action = new CheckTierDuplicates(); - - $prices = collect([ - new TierPriceData('GROUP', Money::of(1, 'EUR'), 1, 0), - new TierPriceData('GROUP', Money::of(1, 'EUR'), 1, 0), - ]); - - MagentoPrice::query()->create([ - 'sku' => '::sku::', - ]); - - $this->expectException(DuplicateTierPriceException::class); - - $action->check('::sku::', $prices); - } -} diff --git a/tests/Actions/DeterminePricesChangedTest.php b/tests/Actions/DeterminePricesChangedTest.php deleted file mode 100644 index e747fb4..0000000 --- a/tests/Actions/DeterminePricesChangedTest.php +++ /dev/null @@ -1,286 +0,0 @@ -assertEquals( - $equals, - $a->equals($b) - ); - } - - public static function dataProvider(): array - { - return [ - 'Unchanged' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'equals' => true, - ], - 'Base added' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - new BasePriceData(Money::of(111, 'EUR'), 1), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]) - ), - 'equals' => false, - ], - 'Base added, other store id' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - new BasePriceData(Money::of(111, 'EUR'), 1), - ]), - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - new BasePriceData(Money::of(10, 'EUR'), 2), - ]), - ), - 'equals' => false, - ], - 'Base changed' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(11, 'EUR')), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]) - ), - 'equals' => false, - ], - 'Tier removed' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - ), - 'equals' => false, - ], - 'Tier changed' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(6, 'EUR'), 20, 1), - ]), - ), - 'equals' => false, - ], - 'Tier added, same count' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1), - new TierPriceData('::group_2::', Money::of(7, 'EUR'), 20, 1), - ]), - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect([ - new TierPriceData('::group_1::', Money::of(9, 'EUR')), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(8, 'EUR'), 1, 2), - new TierPriceData('::group_2::', Money::of(6, 'EUR'), 20, 1), - ]), - ), - 'equals' => false, - ], - 'Special changed' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(5, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'equals' => false, - ], - 'Special date' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(5, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(5, 'EUR'), 2, now()->subDay(), now()->addWeek()), - ]) - ), - 'equals' => false, - ], - 'Special added' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(5, 'EUR'), 2, now()->subDay(), now()->addDay()), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - new SpecialPriceData(Money::of(6, 'EUR'), 1, now()->subDay(), now()->addDay()), - ]) - ), - 'equals' => false, - ], - 'Special added, other store' => [ - 'a' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - new SpecialPriceData(Money::of(6, 'EUR'), 1, now()->subDay(), now()->addDay()), - ]) - ), - 'b' => new PriceData( - '::sku::', - collect([ - new BasePriceData(Money::of(10, 'EUR')), - ]), - collect(), - collect([ - new SpecialPriceData(Money::of(6, 'EUR'), 2, now()->subDay(), now()->addDay()), - new SpecialPriceData(Money::of(6, 'EUR'), 3, now()->subDay(), now()->addDay()), - ]) - ), - 'equals' => false, - ], - ]; - } -} diff --git a/tests/Actions/FindProductsWithMissingPricesTest.php b/tests/Actions/FindProductsWithMissingPricesTest.php deleted file mode 100644 index 8d66491..0000000 --- a/tests/Actions/FindProductsWithMissingPricesTest.php +++ /dev/null @@ -1,41 +0,0 @@ - Http::response([ - 'items' => [ - [ - 'sku' => '::sku_1::', - 'price' => 10, - 'type_id' => 'simple', - ], - [ - 'sku' => '::sku_2::', - 'price' => 0, - 'type_id' => 'simple', - ], - [ - 'sku' => '::sku_3::', - 'type_id' => 'simple', - ], - ], - ]), - ]); - - /** @var FindProductsWithMissingPrices $action */ - $action = app(FindProductsWithMissingPrices::class); - - $prices = $action->retrieve()->all(); - - $this->assertEquals(['::sku_2::', '::sku_3::'], $prices); - } -} diff --git a/tests/Actions/MagentoSpecialPriceUpdateTest.php b/tests/Actions/MagentoSpecialPriceUpdateTest.php deleted file mode 100644 index d78afc4..0000000 --- a/tests/Actions/MagentoSpecialPriceUpdateTest.php +++ /dev/null @@ -1,98 +0,0 @@ - Http::response([ - '::response::', - ]), - 'rest/all/V1/products/special-price-delete' => Http::response(), - 'rest/all/V1/products/special-price' => Http::response(), - ]); - - /** @var UpdateMagentoSpecialPrices $action */ - $action = app(UpdateMagentoSpecialPrices::class); - - $from = now()->subDays(30); - $to = now(); - - $specialPrices = collect([ - new SpecialPriceData(Money::of(1, 'EUR'), 0, $from, $to), - ]); - - $price = new PriceData('::sku::', collect(), collect(), $specialPrices); - - $action->update($price); - - Http::assertSentInOrder([ - fn (Request $request) => $request->data() == ['skus' => ['::sku::']], - fn (Request $request) => $request->data() == ['prices' => ['::response::']], - fn (Request $request) => $request->data() == [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 1.0, - 'store_id' => 0, - 'price_from' => $from->toDateTimeString(), - 'price_to' => $to->toDateTimeString(), - ], - ], - ], - ]); - } - - public function test_it_calls_magento_special_price_post_async(): void - { - config()->set('magento-prices.async', true); - - Http::fake([ - 'rest/all/V1/products/special-price-information' => Http::response([ - '::response::', - ]), - 'rest/all/V1/products/special-price-delete' => Http::response(), - 'rest/all/async/V1/products/special-price' => Http::response(), - ]); - - /** @var UpdateMagentoSpecialPrices $action */ - $action = app(UpdateMagentoSpecialPrices::class); - - $from = now()->subDays(30); - $to = now(); - - $specialPrices = collect([ - new SpecialPriceData(Money::of(1, 'EUR'), 0, $from, $to), - ]); - - $price = new PriceData('::sku::', collect(), collect(), $specialPrices); - - $action->update($price); - - Http::assertSentInOrder([ - fn (Request $request) => $request->data() == ['skus' => ['::sku::']], - fn (Request $request) => $request->data() == ['prices' => ['::response::']], - fn (Request $request) => $request->data() == [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 1.0, - 'store_id' => 0, - 'price_from' => $from->toDateTimeString(), - 'price_to' => $to->toDateTimeString(), - ], - ], - ], - ]); - } -} diff --git a/tests/Actions/MagentoUpdateTest.php b/tests/Actions/MagentoUpdateTest.php deleted file mode 100644 index 0ca3d3c..0000000 --- a/tests/Actions/MagentoUpdateTest.php +++ /dev/null @@ -1,182 +0,0 @@ - Http::response([]), - ]); - - $price = new PriceData('::sku::', collect([new BasePriceData(Money::of(10, 'EUR'))])); - - /** @var UpdateMagentoBasePrice $action */ - $action = app(UpdateMagentoBasePrice::class); - $action->update($price); - - Http::assertSent(function (Request $request): bool { - return $request->data() === [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'store_id' => 0, - ], - ], - ]; - }); - } - - public function test_it_calls_magento_base_price_post_async(): void - { - config()->set('magento-prices.async', true); - - Http::fake([ - 'rest/all/async/V1/products/base-prices*' => Http::response([]), - ]); - - $price = new PriceData('::sku::', collect([new BasePriceData(Money::of(10, 'EUR'))])); - - /** @var UpdateMagentoBasePrice $action */ - $action = app(UpdateMagentoBasePrice::class); - $action->update($price); - - Http::assertSent(function (Request $request): bool { - return $request->data() === [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'store_id' => 0, - ], - ], - ]; - }); - } - - public function test_it_calls_magento_tier_price_post(): void - { - Bus::fake([ - ImportCustomerGroupsJob::class, - ]); - - Http::fake([ - 'rest/all/V1/products/tier-prices*' => Http::response([]), - ]); - - MagentoCustomerGroup::query()->create([ - 'code' => 'GROUP', - 'data' => [], - ]); - - $price = new PriceData('::sku::', collect(), collect([ - new TierPriceData('GROUP', Money::of(10, 'EUR')), - new TierPriceData('GROUP2', Money::of(10, 'EUR')), - ])); - - /** @var UpdateMagentoTierPrices $action */ - $action = app(UpdateMagentoTierPrices::class); - $action->update($price); - - Http::assertSent(function (Request $request): bool { - return $request->data() === [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'website_id' => 0, - 'quantity' => 1, - 'customer_group' => 'GROUP', - 'price_type' => 'fixed', - ], - ], - ]; - }); - - Bus::assertDispatched(ImportCustomerGroupsJob::class); - } - - public function test_it_calls_magento_tier_price_post_async(): void - { - config()->set('magento-prices.async', true); - - Bus::fake([ - ImportCustomerGroupsJob::class, - ]); - - Http::fake([ - 'rest/all/async/V1/products/tier-prices*' => Http::response([]), - ]); - - MagentoCustomerGroup::query()->create([ - 'code' => 'GROUP', - 'data' => [], - ]); - - $price = new PriceData('::sku::', collect(), collect([ - new TierPriceData('GROUP', Money::of(10, 'EUR')), - new TierPriceData('GROUP2', Money::of(10, 'EUR')), - ])); - - /** @var UpdateMagentoTierPrices $action */ - $action = app(UpdateMagentoTierPrices::class); - $action->update($price); - - Http::assertSent(function (Request $request): bool { - return $request->data() === [ - 'prices' => [ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'website_id' => 0, - 'quantity' => 1, - 'customer_group' => 'GROUP', - 'price_type' => 'fixed', - ], - ], - ]; - }); - - Bus::assertDispatched(ImportCustomerGroupsJob::class); - } - - public function test_it_can_throw_exceptions_if_groups_are_unavailable(): void - { - $this->expectException(PriceUpdateException::class); - - Bus::fake([ - ImportCustomerGroupsJob::class, - ]); - - Http::fake([ - 'rest/all/V1/products/tier-prices*' => Http::response([]), - ]); - - $price = new PriceData('::sku::', collect(), collect([ - new TierPriceData('GROUP', Money::of(10, 'EUR')), - new TierPriceData('GROUP2', Money::of(10, 'EUR')), - ])); - - /** @var UpdateMagentoTierPrices $action */ - $action = app(UpdateMagentoTierPrices::class); - $action->update($price); - - Bus::assertDispatched(ImportCustomerGroupsJob::class); - } -} diff --git a/tests/Actions/MonitorWaitTimesTest.php b/tests/Actions/MonitorWaitTimesTest.php deleted file mode 100644 index eee2a13..0000000 --- a/tests/Actions/MonitorWaitTimesTest.php +++ /dev/null @@ -1,112 +0,0 @@ -set('magento-prices.retrieve_limit', 10); - config()->set('magento-prices.monitor.retrieval_max_wait', 5); - - for ($i = 0; $i < 100; $i++) { - MagentoPrice::query()->create([ - 'sku' => $i, - 'sync' => true, - 'retrieve' => true, - 'update' => false, - ]); - } - - /** @var MonitorWaitTimes $action */ - $action = app(MonitorWaitTimes::class); - - $action->monitor(); - - Event::assertDispatched(LongWaitDetectedEvent::class, function (LongWaitDetectedEvent $event) { - return $event->type === 'retrieve' && $event->wait === 10; - }); - } - - public function test_retrieve_wait_times_does_not_dispatch_event(): void - { - Event::fake(); - - config()->set('magento-prices.retrieve_limit', 10); - config()->set('magento-prices.monitor.retrieval_max_wait', 10); - - for ($i = 0; $i < 100; $i++) { - MagentoPrice::query()->create([ - 'sku' => $i, - 'sync' => true, - 'retrieve' => true, - 'update' => false, - ]); - } - - /** @var MonitorWaitTimes $action */ - $action = app(MonitorWaitTimes::class); - - $action->monitor(); - - Event::assertNotDispatched(LongWaitDetectedEvent::class); - } - - public function test_update_wait_times_dispatches_event(): void - { - Event::fake(); - - config()->set('magento-prices.update_limit', 10); - config()->set('magento-prices.monitor.update_max_wait', 5); - - for ($i = 0; $i < 100; $i++) { - MagentoPrice::query()->create([ - 'sku' => $i, - 'sync' => true, - 'retrieve' => false, - 'update' => true, - ]); - } - - /** @var MonitorWaitTimes $action */ - $action = app(MonitorWaitTimes::class); - - $action->monitor(); - - Event::assertDispatched(LongWaitDetectedEvent::class, function (LongWaitDetectedEvent $event) { - return $event->type === 'update' && $event->wait === 10; - }); - } - - public function test_update_wait_times_does_not_dispatch_event(): void - { - Event::fake(); - - config()->set('magento-prices.update_limit', 10); - config()->set('magento-prices.monitor.update_max_wait', 10); - - for ($i = 0; $i < 100; $i++) { - MagentoPrice::query()->create([ - 'sku' => $i, - 'sync' => true, - 'retrieve' => false, - 'update' => true, - ]); - } - - /** @var MonitorWaitTimes $action */ - $action = app(MonitorWaitTimes::class); - - $action->monitor(); - - Event::assertNotDispatched(LongWaitDetectedEvent::class); - } -} diff --git a/tests/Actions/ProcessPriceTest.php b/tests/Actions/ProcessPriceTest.php deleted file mode 100644 index cecd59f..0000000 --- a/tests/Actions/ProcessPriceTest.php +++ /dev/null @@ -1,67 +0,0 @@ -action = new ProcessPrice(new CheckMagentoExistenceMock()); - - $basePrices = collect([ - new BasePriceData(Money::of(10, 'EUR'), 0), - ]); - - $tierPrices = collect([ - new TierPriceData('::group_1::', Money::of(10, 'EUR'), 0), - ]); - - $this->data = new PriceData('::sku::', $basePrices, $tierPrices); - - MagentoPrice::create([ - 'sync' => false, - 'sku' => '::sku::', - 'base_prices' => $basePrices, - 'tier_prices' => $tierPrices, - ]); - } - - public function test_it_does_not_set_update_if_no_changes(): void - { - $dto = $this->data->getModel()->getData(); - - $this->action->process($dto); - - $this->assertEquals(false, $this->data->getModel()->update); - } - - public function test_it_sets_update(): void - { - $this->data->basePrices = collect([ - new BasePriceData(Money::of(11, 'EUR'), 0), - ]); - - $this->action->process($this->data); - - $this->assertEquals(true, $this->data->getModel()->update); - $this->assertEquals(false, $this->data->getModel()->retrieve); - } -} diff --git a/tests/Actions/ProcessPricesTest.php b/tests/Actions/ProcessPricesTest.php new file mode 100644 index 0000000..17104bd --- /dev/null +++ b/tests/Actions/ProcessPricesTest.php @@ -0,0 +1,69 @@ +create([ + 'sku' => '::sku::', + 'retrieve' => true, + ]); + + /** @var ProcessPrices $action */ + $action = app(ProcessPrices::class); + $action->process(); + + Bus::assertDispatched(RetrievePriceJob::class); + Bus::assertNotDispatched(UpdatePriceJob::class); + } + + #[Test] + public function it_dispatches_update_jobs(): void + { + Bus::fake(); + + Price::query()->create([ + 'sku' => '::sku::', + 'update' => true, + ]); + + /** @var ProcessPrices $action */ + $action = app(ProcessPrices::class); + $action->process(); + + Bus::assertNotDispatched(RetrievePriceJob::class); + Bus::assertDispatched(UpdatePriceJob::class); + } + + #[Test] + public function it_dispatches_async_update_job(): void + { + Bus::fake(); + config()->set('magento-prices.async', true); + + Price::query()->create([ + 'sku' => '::sku::', + 'update' => true, + ]); + + /** @var ProcessPrices $action */ + $action = app(ProcessPrices::class); + $action->process(); + + Bus::assertDispatched(UpdatePricesAsyncJob::class); + } +} diff --git a/tests/Actions/Retrieval/RetrieveAllPricesTest.php b/tests/Actions/Retrieval/RetrieveAllPricesTest.php new file mode 100644 index 0000000..5de6a81 --- /dev/null +++ b/tests/Actions/Retrieval/RetrieveAllPricesTest.php @@ -0,0 +1,29 @@ +set('magento-prices.repository', FakeRepository::class); + Bus::fake(); + + MagentoProduct::query()->create(['sku' => '::sku::', 'exists_in_magento' => true]); + + /** @var RetrieveAllPrices $action */ + $action = app(RetrieveAllPrices::class); + $action->retrieve(null); + + Bus::assertDispatched(RetrievePriceJob::class); + } +} diff --git a/tests/Actions/Retrieval/RetrievePriceTest.php b/tests/Actions/Retrieval/RetrievePriceTest.php new file mode 100644 index 0000000..159a503 --- /dev/null +++ b/tests/Actions/Retrieval/RetrievePriceTest.php @@ -0,0 +1,54 @@ +set('magento-prices.repository', FakeNullRepository::class); + + /** @var Price $model */ + $model = Price::query() + ->create([ + 'sku' => '::sku::', + 'retrieve' => true, + ]); + + /** @var RetrievePrice $action */ + $action = app(RetrievePrice::class); + $action->retrieve('::sku::', false); + + $this->assertFalse($model->refresh()->retrieve); + } + + #[Test] + public function it_dispatches_save_job(): void + { + config()->set('magento-prices.repository', FakeRepository::class); + Bus::fake(); + + Price::query() + ->create([ + 'sku' => '::sku::', + ]); + + /** @var RetrievePrice $action */ + $action = app(RetrievePrice::class); + $action->retrieve('::sku::', true); + + Bus::assertDispatched(SavePriceJob::class, function (SavePriceJob $job): bool { + return $job->data['sku'] === '::sku::' && $job->forceUpdate; + }); + } +} diff --git a/tests/Actions/Retrieval/SavePriceTest.php b/tests/Actions/Retrieval/SavePriceTest.php new file mode 100644 index 0000000..e971364 --- /dev/null +++ b/tests/Actions/Retrieval/SavePriceTest.php @@ -0,0 +1,175 @@ + '::sku::', + 'base_prices' => [ + [ + 'store_id' => 0, + 'price' => 10, + ], + ], + 'tier_prices' => [ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + ], + 'special_prices' => [ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], + ]); + + /** @var SavePrice $action */ + $action = app(SavePrice::class); + $action->save($priceData, false); + + /** @var Price $model */ + $model = Price::query()->firstWhere('sku', '=', '::sku::'); + + $this->assertNotNull($model->base_prices); + $this->assertNotNull($model->tier_prices); + $this->assertNotNull($model->special_prices); + $this->assertEquals([['store_id' => 0, 'price' => 10]], $model->base_prices); + $this->assertEquals([ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + ], $model->tier_prices); + + $this->assertEquals([ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], $model->special_prices); + + $this->assertTrue($model->sync); + $this->assertFalse($model->retrieve); + $this->assertTrue($model->update); + $this->assertNotNull($model->last_retrieved); + $this->assertEquals('27f10836349f35baf9aa229f963e4ddf', $model->checksum); + + } + + #[Test] + public function it_does_not_set_update_when_unchanged(): void + { + $priceData = PriceData::of([ + 'sku' => '::sku::', + 'base_prices' => [ + [ + 'store_id' => 0, + 'price' => 10, + ], + ], + 'tier_prices' => [ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + ], + 'special_prices' => [ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], + ]); + + /** @var SavePrice $action */ + $action = app(SavePrice::class); + $action->save($priceData, false); + + /** @var Price $model */ + $model = Price::query()->firstWhere('sku', '=', '::sku::'); + + $this->assertTrue($model->update); + + $model->update(['update' => false]); + + $action->save($priceData, false); + + $this->assertFalse($model->refresh()->update); + } + + #[Test] + public function it_can_force_update(): void + { + $priceData = PriceData::of([ + 'sku' => '::sku::', + 'base_prices' => [ + [ + 'store_id' => 0, + 'price' => 10, + ], + ], + 'tier_prices' => [ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + ], + 'special_prices' => [ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], + ]); + + /** @var SavePrice $action */ + $action = app(SavePrice::class); + $action->save($priceData, false); + + /** @var Price $model */ + $model = Price::query()->firstWhere('sku', '=', '::sku::'); + + $this->assertTrue($model->update); + + $model->update(['update' => false]); + + $action->save($priceData, true); + + $this->assertTrue($model->refresh()->update); + } +} diff --git a/tests/Actions/SyncPricesTest.php b/tests/Actions/SyncPricesTest.php deleted file mode 100644 index 9aa42dc..0000000 --- a/tests/Actions/SyncPricesTest.php +++ /dev/null @@ -1,72 +0,0 @@ - '::sku_1::', - 'retrieve' => true, - 'update' => true, - ]); - - MagentoPrice::create([ - 'sku' => '::sku_2::', - 'retrieve' => true, - 'update' => true, - ]); - - $action = new SyncPrices(); - $action->sync(); - - $this->assertEquals(2, MagentoPrice::shouldRetrieve()->count()); - $this->assertEquals(0, MagentoPrice::shouldUpdate()->count()); - } - - public function test_it_dispatches_single_retrieve_job(): void - { - MagentoPrice::create([ - 'sku' => '::sku_1::', - 'retrieve' => true, - 'update' => false, - ]); - - $action = new SyncPrices(); - $action->sync(); - - Bus::assertDispatched(RetrievePriceJob::class); - } - - public function test_it_dispatches_single_update_job(): void - { - MagentoPrice::create([ - 'sku' => '::sku_1::', - 'retrieve' => false, - 'update' => true, - ]); - - $action = new SyncPrices(); - $action->sync(); - - Bus::assertDispatched(UpdatePriceJob::class); - } -} diff --git a/tests/Actions/Update/Async/UpdateBasePricesAsyncTest.php b/tests/Actions/Update/Async/UpdateBasePricesAsyncTest.php new file mode 100644 index 0000000..8df373d --- /dev/null +++ b/tests/Actions/Update/Async/UpdateBasePricesAsyncTest.php @@ -0,0 +1,126 @@ + Http::response([ + 'bulk_uuid' => '::uuid::', + 'request_items' => [ + [ + 'id' => 0, + 'status' => 'accepted', + ], + [ + 'id' => 1, + 'status' => 'accepted', + ], + ], + ]), + ])->preventStrayRequests(); + + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_1::', + 'base_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_2::', + 'base_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + ], + [ + 'store_id' => 2, + 'price' => 20, + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_3::', + 'base_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + 'base_prices' => [], + ]), + ]); + + /** @var UpdateBasePricesAsync $action */ + $action = app(UpdateBasePricesAsync::class); + $action->update($models); + + Http::assertSent(function (Request $request): bool { + return $request->data() === [ + [ + 'prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'sku' => '::sku_1::', + ], + ], + ], + [ + 'prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'sku' => '::sku_2::', + ], + [ + 'store_id' => 2, + 'price' => 20, + 'sku' => '::sku_2::', + ], + ], + ], + ]; + }); + } + + #[Test] + public function it_does_nothing_when_all_prices_are_rejected(): void + { + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_3::', + 'base_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + ]), + ]); + + /** @var UpdateBasePricesAsync $action */ + $action = app(UpdateBasePricesAsync::class); + $action->update($models); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Update/Async/UpdatePricesAsyncTest.php b/tests/Actions/Update/Async/UpdatePricesAsyncTest.php new file mode 100644 index 0000000..9c865cb --- /dev/null +++ b/tests/Actions/Update/Async/UpdatePricesAsyncTest.php @@ -0,0 +1,33 @@ +mock(UpdatesBasePricesAsync::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->once()->andReturnTrue(); + }); + + $this->mock(UpdatesTierPricesAsync::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->once()->andReturnTrue(); + }); + + $this->mock(UpdatesSpecialPricesAsync::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->once()->andReturnTrue(); + }); + /** @var UpdatePricesAsync $action */ + $action = app(UpdatePricesAsync::class); + $action->update(collect()); + } +} diff --git a/tests/Actions/Update/Async/UpdateSpecialPricesAsyncTest.php b/tests/Actions/Update/Async/UpdateSpecialPricesAsyncTest.php new file mode 100644 index 0000000..232ae3f --- /dev/null +++ b/tests/Actions/Update/Async/UpdateSpecialPricesAsyncTest.php @@ -0,0 +1,129 @@ +mock(DeletesCurrentSpecialPrices::class, function (MockInterface $mock): void { + $mock->shouldReceive('delete')->twice()->andReturn(); + }); + + Magento::fake(); + + Http::fake([ + 'magento/rest/all/async/bulk/V1/products/special-price' => Http::response([ + 'bulk_uuid' => '::uuid::', + 'request_items' => [ + [ + 'id' => 0, + 'status' => 'accepted', + ], + [ + 'id' => 1, + 'status' => 'accepted', + ], + ], + ]), + ])->preventStrayRequests(); + + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_1::', + 'has_special' => true, + 'special_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_2::', + 'has_special' => false, + 'special_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_3::', + 'has_special' => true, + 'special_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + 'special_prices' => [], + ]), + ]); + + /** @var UpdateSpecialPricesAsync $action */ + $action = app(UpdateSpecialPricesAsync::class); + $action->update($models); + + Http::assertSent(function (Request $request): bool { + return $request->data() === [ + [ + 'prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + 'sku' => '::sku_1::', + ], + ], + ], + [ + 'prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + 'sku' => '::sku_2::', + ], + ], + ], + ]; + }); + } + + #[Test] + public function it_does_nothing_when_all_prices_are_rejected(): void + { + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_3::', + 'special_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + ]), + ]); + + /** @var UpdateSpecialPricesAsync $action */ + $action = app(UpdateSpecialPricesAsync::class); + $action->update($models); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Update/Async/UpdateTierPricesAsyncTest.php b/tests/Actions/Update/Async/UpdateTierPricesAsyncTest.php new file mode 100644 index 0000000..a18c13e --- /dev/null +++ b/tests/Actions/Update/Async/UpdateTierPricesAsyncTest.php @@ -0,0 +1,157 @@ +mock(RetrievesCustomerGroups::class, function (MockInterface $mock): void { + $mock->shouldReceive('retrieve')->andReturn(['GENERAL', 'RETAIL']); + }); + } + + #[Test] + public function it_updates_tier_prices_async(): void + { + Http::fake([ + 'magento/rest/all/async/bulk/V1/products/tier-prices' => Http::response([ + 'bulk_uuid' => '::uuid::', + 'request_items' => [ + [ + 'id' => 0, + 'status' => 'accepted', + ], + [ + 'id' => 1, + 'status' => 'accepted', + ], + ], + ]), + ])->preventStrayRequests(); + + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_1::', + 'tier_prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_2::', + 'tier_prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + ], + ], + ]), + Price::query()->create([ + 'sku' => '::sku_3::', + 'tier_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + 'tier_prices' => [], + ]), + ]); + + /** @var UpdateTierPricesAsync $action */ + $action = app(UpdateTierPricesAsync::class); + $action->update($models); + + Http::assertSent(function (Request $request): bool { + return $request->data() === [ + [ + 'prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + 'sku' => '::sku_1::', + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + 'sku' => '::sku_1::', + ], + ], + ], + [ + 'prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + 'sku' => '::sku_2::', + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + 'sku' => '::sku_2::', + ], + ], + ], + ]; + }); + } + + #[Test] + public function it_does_nothing_when_all_prices_are_rejected(): void + { + $models = collect([ + Price::query()->create([ + 'sku' => '::sku_3::', + 'tier_prices' => [], + ]), + Price::query()->create([ + 'sku' => '::sku_4::', + ]), + ]); + + /** @var UpdateTierPricesAsync $action */ + $action = app(UpdateTierPricesAsync::class); + $action->update($models); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Update/Sync/UpdateBasePriceTest.php b/tests/Actions/Update/Sync/UpdateBasePriceTest.php new file mode 100644 index 0000000..50aaf84 --- /dev/null +++ b/tests/Actions/Update/Sync/UpdateBasePriceTest.php @@ -0,0 +1,107 @@ + Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'base_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + ], + ], + ]); + + /** @var UpdateBasePrice $action */ + $action = app(UpdateBasePrice::class); + $this->assertTrue($action->update($model)); + } + + #[Test] + public function it_returns_false_on_failure(): void + { + Http::fake([ + 'magento/rest/all/V1/products/base-prices' => Http::response(null, 500), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'base_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + ], + ], + ]); + + /** @var UpdateBasePrice $action */ + $action = app(UpdateBasePrice::class); + $this->assertFalse($action->update($model)); + } + + #[Test] + public function it_removes_base_prices(): void + { + Http::fake([ + 'magento/rest/all/V1/products/base-prices' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'base_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + ], + ], + ]); + + /** @var UpdateBasePrice $action */ + $action = app(UpdateBasePrice::class); + $this->assertTrue($action->update($model)); + } + + #[Test] + public function it_only_removes_base_prices_once(): void + { + Http::fake()->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'base_prices' => [], + ]); + + /** @var UpdateBasePrice $action */ + $action = app(UpdateBasePrice::class); + $this->assertTrue($action->update($model)); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Update/Sync/UpdatePriceTest.php b/tests/Actions/Update/Sync/UpdatePriceTest.php new file mode 100644 index 0000000..fe21754 --- /dev/null +++ b/tests/Actions/Update/Sync/UpdatePriceTest.php @@ -0,0 +1,119 @@ +mock(ChecksMagentoExistence::class, function (MockInterface $mock): void { + $mock->shouldReceive('exists')->with('::sku::')->andReturnFalse(); + }); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'update' => true, + ]); + + /** @var UpdatePrice $action */ + $action = app(UpdatePrice::class); + $action->update($model); + + $this->assertFalse($model->refresh()->update); + } + + #[Test] + public function it_calls_update_actions(): void + { + Event::fake([UpdatedPriceEvent::class]); + + $this->mock(ChecksMagentoExistence::class, function (MockInterface $mock): void { + $mock->shouldReceive('exists')->with('::sku::')->andReturnTrue(); + }); + + $this->mock(UpdatesBasePrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnTrue(); + }); + + $this->mock(UpdatesTierPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnTrue(); + }); + + $this->mock(UpdatesSpecialPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnTrue(); + }); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'update' => true, + ]); + + /** @var UpdatePrice $action */ + $action = app(UpdatePrice::class); + $action->update($model); + + $model->refresh(); + + $this->assertFalse($model->update); + $this->assertNotNull($model->last_updated); + + Event::assertDispatched(UpdatedPriceEvent::class); + } + + #[Test] + public function it_registers_failure_when_once_call_fails(): void + { + Event::fake([UpdatedPriceEvent::class]); + + $this->mock(ChecksMagentoExistence::class, function (MockInterface $mock): void { + $mock->shouldReceive('exists')->with('::sku::')->andReturnTrue(); + }); + + $this->mock(UpdatesBasePrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnTrue(); + }); + + $this->mock(UpdatesTierPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnTrue(); + }); + + $this->mock(UpdatesSpecialPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->andReturnFalse(); + }); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'update' => true, + 'fail_count' => 0, + 'last_failed' => null, + ]); + + /** @var UpdatePrice $action */ + $action = app(UpdatePrice::class); + $action->update($model); + + $model->refresh(); + + $this->assertTrue($model->update); + $this->assertNotNull($model->last_failed); + $this->assertEquals(1, $model->fail_count); + + Event::assertNotDispatched(UpdatedPriceEvent::class); + } +} diff --git a/tests/Actions/Update/Sync/UpdateSpecialPriceTest.php b/tests/Actions/Update/Sync/UpdateSpecialPriceTest.php new file mode 100644 index 0000000..f9c670a --- /dev/null +++ b/tests/Actions/Update/Sync/UpdateSpecialPriceTest.php @@ -0,0 +1,119 @@ + Http::response(), + 'magento/rest/all/V1/products/special-price-information' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => false, + 'special_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + ], + ], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + $this->assertTrue($action->update($model)); + $this->assertTrue($model->refresh()->has_special); + } + + #[Test] + public function it_returns_false_on_failure(): void + { + Http::fake([ + 'magento/rest/all/V1/products/special-price' => Http::response(null, 500), + 'magento/rest/all/V1/products/special-price-information' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => true, + 'special_prices' => [ + [ + 'store_id' => 1, + 'price' => 10, + 'from' => '2024-07-30', + 'to' => '2024-08-30', + ], + ], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + $this->assertFalse($action->update($model)); + } + + #[Test] + public function it_removes_special_prices(): void + { + Http::fake([ + 'magento/rest/all/V1/products/special-price-information' => Http::response([ + [ + 'price', + ], + ]), + 'magento/rest/all/V1/products/special-price-delete' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => true, + 'special_prices' => [], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + $action->update($model); + $this->assertFalse($model->refresh()->has_special); + } + + #[Test] + public function it_only_removes_special_prices_once(): void + { + Http::fake()->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => false, + 'special_prices' => [], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + $this->assertTrue($action->update($model)); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Update/Sync/UpdateTierPriceTest.php b/tests/Actions/Update/Sync/UpdateTierPriceTest.php new file mode 100644 index 0000000..10b3e9a --- /dev/null +++ b/tests/Actions/Update/Sync/UpdateTierPriceTest.php @@ -0,0 +1,124 @@ +mock(RetrievesCustomerGroups::class, function (MockInterface $mock): void { + $mock->shouldReceive('retrieve')->andReturn(['GENERAL', 'RETAIL']); + }); + } + + #[Test] + public function it_updates_tier_price(): void + { + Http::fake([ + 'magento/rest/all/V1/products/tier-prices' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'tier_prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + ], + ], + ]); + + /** @var UpdateTierPrice $action */ + $action = app(UpdateTierPrice::class); + $this->assertTrue($action->update($model)); + } + + #[Test] + public function it_returns_false_on_failure(): void + { + Http::fake([ + 'magento/rest/all/V1/products/tier-prices' => Http::response(null, 500), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'tier_prices' => [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 8, + ], + ], + ]); + + /** @var UpdateTierPrice $action */ + $action = app(UpdateTierPrice::class); + $this->assertFalse($action->update($model)); + } + + #[Test] + public function it_removes_tier_prices(): void + { + Http::fake([ + 'magento/rest/all/V1/products/tier-prices' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'tier_prices' => [], + ]); + + /** @var UpdateTierPrice $action */ + $action = app(UpdateTierPrice::class); + $this->assertTrue($action->update($model)); + } + + #[Test] + public function it_only_removes_tier_prices_once(): void + { + Http::fake()->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'tier_prices' => [], + ]); + + /** @var UpdateTierPrice $action */ + $action = app(UpdateTierPrice::class); + $this->assertTrue($action->update($model)); + + Http::assertNothingSent(); + } +} diff --git a/tests/Actions/Utility/CheckTierDuplicatesTest.php b/tests/Actions/Utility/CheckTierDuplicatesTest.php new file mode 100644 index 0000000..71ef558 --- /dev/null +++ b/tests/Actions/Utility/CheckTierDuplicatesTest.php @@ -0,0 +1,68 @@ +create(['sku' => '::sku::']); + + $tierPrices = [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'RETAIL', + 'price' => 10, + ], + ]; + + /** @var CheckTierDuplicates $action */ + $action = app(CheckTierDuplicates::class); + $action->check($model, $tierPrices); + + $this->assertTrue(true, 'Passed'); + } + + #[Test] + public function it_throws_exception(): void + { + /** @var Price $model */ + $model = Price::query()->create(['sku' => '::sku::']); + + $tierPrices = [ + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 10, + ], + [ + 'website_id' => 1, + 'quantity' => 1, + 'customer_group' => 'GENERAL', + 'price' => 20, + ], + ]; + + $this->expectException(DuplicateTierPriceException::class); + + /** @var CheckTierDuplicates $action */ + $action = app(CheckTierDuplicates::class); + $action->check($model, $tierPrices); + } +} diff --git a/tests/Actions/Utility/DeleteCurrentSpecialPricesTest.php b/tests/Actions/Utility/DeleteCurrentSpecialPricesTest.php new file mode 100644 index 0000000..5150d0b --- /dev/null +++ b/tests/Actions/Utility/DeleteCurrentSpecialPricesTest.php @@ -0,0 +1,65 @@ + Http::response([ + [ + 'price', + ], + ]), + 'magento/rest/all/V1/products/special-price-delete' => Http::response(), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => true, + 'special_prices' => [], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + $action->update($model); + $this->assertFalse($model->refresh()->has_special); + } + + #[Test] + public function it_throws_exception_when_special_price_removal_fails(): void + { + Http::fake([ + 'magento/rest/all/V1/products/special-price-information' => Http::response([ + [ + 'price', + ], + ]), + 'magento/rest/all/V1/products/special-price-delete' => Http::response(null, 500), + ])->preventStrayRequests(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'has_special' => true, + 'special_prices' => [], + ]); + + /** @var UpdateSpecialPrice $action */ + $action = app(UpdateSpecialPrice::class); + + $this->expectException(RequestException::class); + + $action->update($model); + } +} diff --git a/tests/Actions/ImportCustomerGroupsTest.php b/tests/Actions/Utility/ImportCustomerGroupsTest.php similarity index 83% rename from tests/Actions/ImportCustomerGroupsTest.php rename to tests/Actions/Utility/ImportCustomerGroupsTest.php index 03970b4..1d29a6f 100644 --- a/tests/Actions/ImportCustomerGroupsTest.php +++ b/tests/Actions/Utility/ImportCustomerGroupsTest.php @@ -1,17 +1,18 @@ import(); - $count = MagentoCustomerGroup::query()->count(); + $count = CustomerGroup::query()->count(); $this->assertEquals(3, $count); } - /** @test */ + #[Test] public function it_can_add_customer_groups(): void { Magento::fake(); @@ -80,13 +81,13 @@ public function it_can_add_customer_groups(): void ], ]); - MagentoCustomerGroup::query()->create([ + CustomerGroup::query()->create([ 'code' => 'General', 'data' => [], ]); - /** @var MagentoPrice $price */ - $price = MagentoPrice::query()->create([ + /** @var Price $price */ + $price = Price::query()->create([ 'sku' => '1000', 'update' => false, ]); @@ -95,7 +96,7 @@ public function it_can_add_customer_groups(): void $action = app(ImportCustomerGroups::class); $action->import(); - $count = MagentoCustomerGroup::query()->count(); + $count = CustomerGroup::query()->count(); $this->assertEquals(3, $count); @@ -104,7 +105,7 @@ public function it_can_add_customer_groups(): void $this->assertTrue($price->update); } - /** @test */ + #[Test] public function it_can_delete_customer_groups(): void { Magento::fake(); @@ -115,13 +116,13 @@ public function it_can_delete_customer_groups(): void ], ]); - MagentoCustomerGroup::query()->create([ + CustomerGroup::query()->create([ 'code' => 'Delete', 'data' => [], ]); - /** @var MagentoPrice $price */ - $price = MagentoPrice::query()->create([ + /** @var Price $price */ + $price = Price::query()->create([ 'sku' => '1000', 'sync' => false, 'update' => false, @@ -131,7 +132,7 @@ public function it_can_delete_customer_groups(): void $action = app(ImportCustomerGroups::class); $action->import(); - $count = MagentoCustomerGroup::query()->count(); + $count = CustomerGroup::query()->count(); $this->assertEquals(0, $count); diff --git a/tests/Actions/Utility/ProcessProductsWithMissingPricesTest.php b/tests/Actions/Utility/ProcessProductsWithMissingPricesTest.php new file mode 100644 index 0000000..7777c49 --- /dev/null +++ b/tests/Actions/Utility/ProcessProductsWithMissingPricesTest.php @@ -0,0 +1,95 @@ + Http::response([ + 'items' => [ + [ + 'sku' => '::sku_1::', + 'price' => 10, + 'type_id' => 'simple', + ], + [ + 'sku' => '::sku_2::', + 'price' => 0, + 'type_id' => 'simple', + ], + [ + 'sku' => '::sku_3::', + 'type_id' => 'simple', + ], + ], + ]), + ])->preventStrayRequests(); + + Price::query()->create(['sku' => '::sku_3::']); + + /** @var ProcessProductsWithMissingPrices $action */ + $action = app(ProcessProductsWithMissingPrices::class); + $action->process(); + + Bus::assertDispatched(UpdatePriceJob::class, function (UpdatePriceJob $job): bool { + return $job->price->sku === '::sku_3::'; + }); + + Bus::assertDispatched(RetrievePriceJob::class, function (RetrievePriceJob $job): bool { + return $job->sku === '::sku_2::'; + }); + } + + #[Test] + public function it_dispatches_update_jobs_async(): void + { + config()->set('magento-prices.async', true); + + Magento::fake(); + Bus::fake(); + + Http::fake([ + 'magento/rest/all/V1/products?fields=sku%2Cprice%2Ctype_id&searchCriteria%5BpageSize%5D=100&searchCriteria%5BcurrentPage%5D=1' => Http::response([ + 'items' => [ + [ + 'sku' => '::sku_1::', + 'price' => 10, + 'type_id' => 'simple', + ], + [ + 'sku' => '::sku_2::', + 'price' => 0, + 'type_id' => 'simple', + ], + ], + ]), + ])->preventStrayRequests(); + + Price::query()->create(['sku' => '::sku_2::']); + + /** @var ProcessProductsWithMissingPrices $action */ + $action = app(ProcessProductsWithMissingPrices::class); + $action->process(); + + Bus::assertDispatched(UpdatePricesAsyncJob::class, function (UpdatePricesAsyncJob $job): bool { + return $job->prices->pluck('sku')->toArray() === ['::sku_2::']; + }); + } +} diff --git a/tests/Actions/Utility/RetrieveCustomerGroupsTest.php b/tests/Actions/Utility/RetrieveCustomerGroupsTest.php new file mode 100644 index 0000000..386efb4 --- /dev/null +++ b/tests/Actions/Utility/RetrieveCustomerGroupsTest.php @@ -0,0 +1,58 @@ +expectException(PriceUpdateException::class); + + /** @var RetrieveCustomerGroups $action */ + $action = app(RetrieveCustomerGroups::class); + $action->retrieve(); + + Bus::assertDispatched(ImportCustomerGroupsJob::class); + } + + #[Test] + public function it_returns_groups(): void + { + CustomerGroup::query()->create(['code' => '::group::', 'data' => []]); + + /** @var RetrieveCustomerGroups $action */ + $action = app(RetrieveCustomerGroups::class); + $this->assertEquals(['::group::', 'ALL GROUPS'], $action->retrieve()); + + Bus::assertDispatched(ImportCustomerGroupsJob::class); + } + + #[Test] + public function it_does_not_dispatch_import_job_twice(): void + { + CustomerGroup::query()->create(['code' => '::group::', 'data' => []]); + + /** @var RetrieveCustomerGroups $action */ + $action = app(RetrieveCustomerGroups::class); + $action->retrieve(); + $action->retrieve(); + + Bus::assertDispatchedTimes(ImportCustomerGroupsJob::class, 1); + } +} diff --git a/tests/Commands/CommandDispatchTest.php b/tests/Commands/CommandDispatchTest.php deleted file mode 100644 index 1319686..0000000 --- a/tests/Commands/CommandDispatchTest.php +++ /dev/null @@ -1,79 +0,0 @@ -artisan($command, $args); - - Bus::assertDispatched($job); - } - - public function test_retrieve_price(): void - { - $this->artisan(RetrievePricesCommand::class, ['sku' => '::sku::']); - - Bus::assertDispatched(RetrievePriceJob::class); - } - - public function test_retrieve_prices(): void - { - $this->artisan(RetrievePricesCommand::class); - - Bus::assertDispatched(RetrievePricesJob::class); - } - - public function test_update_price(): void - { - $this->artisan(UpdatePriceCommand::class, ['sku' => '::sku::']); - - Bus::assertDispatched(UpdatePriceJob::class); - } - - public static function dataProvider(): array - { - return [ - 'Sync prices' => [ - SyncPricesCommand::class, - SyncPricesJob::class, - ], - 'Sync prices sync' => [ - SyncPricesCommand::class, - SyncPricesJob::class, - ['--sync' => true], - ], - 'Search missing' => [ - SearchMissingPricesCommand::class, - SyncMissingPricesJob::class, - ], - 'Monitor wait times' => [ - MonitorWaitTimesCommand::class, - MonitorWaitTimesJob::class, - ], - ]; - } -} diff --git a/tests/Commands/ImportCustomerGroupsCommandTest.php b/tests/Commands/ImportCustomerGroupsCommandTest.php deleted file mode 100644 index 6109ff9..0000000 --- a/tests/Commands/ImportCustomerGroupsCommandTest.php +++ /dev/null @@ -1,27 +0,0 @@ -artisan(ImportCustomerGroupsCommand::class); - - $command - ->assertSuccessful() - ->run(); - - Bus::assertDispatched(ImportCustomerGroupsJob::class); - } -} diff --git a/tests/Commands/ProcessPricesCommandTest.php b/tests/Commands/ProcessPricesCommandTest.php new file mode 100644 index 0000000..675b715 --- /dev/null +++ b/tests/Commands/ProcessPricesCommandTest.php @@ -0,0 +1,22 @@ +artisan(ProcessPricesCommand::class); + + Bus::assertDispatched(ProcessPricesJob::class); + } +} diff --git a/tests/Commands/Retrieval/RetrieveAllPricesCommandTest.php b/tests/Commands/Retrieval/RetrieveAllPricesCommandTest.php new file mode 100644 index 0000000..3332e2f --- /dev/null +++ b/tests/Commands/Retrieval/RetrieveAllPricesCommandTest.php @@ -0,0 +1,22 @@ +artisan(RetrieveAllPricesCommand::class); + + Bus::assertDispatched(RetrieveAllPricesJob::class); + } +} diff --git a/tests/Commands/Retrieval/RetrievePriceCommandTest.php b/tests/Commands/Retrieval/RetrievePriceCommandTest.php new file mode 100644 index 0000000..71dca9b --- /dev/null +++ b/tests/Commands/Retrieval/RetrievePriceCommandTest.php @@ -0,0 +1,22 @@ +artisan(RetrievePriceCommand::class, ['sku' => '::sku::']); + + Bus::assertDispatched(RetrievePriceJob::class); + } +} diff --git a/tests/Commands/Update/UpdateAllPricesCommandTest.php b/tests/Commands/Update/UpdateAllPricesCommandTest.php new file mode 100644 index 0000000..42b7218 --- /dev/null +++ b/tests/Commands/Update/UpdateAllPricesCommandTest.php @@ -0,0 +1,33 @@ +create(['sku' => '::sku_1::', 'exists_in_magento' => true]); + MagentoProduct::query()->create(['sku' => '::sku_2::', 'exists_in_magento' => false]); + MagentoProduct::query()->create(['sku' => '::sku_3::', 'exists_in_magento' => true]); + + Price::query()->create(['sku' => '::sku_1::']); + Price::query()->create(['sku' => '::sku_2::']); + Price::query()->create(['sku' => '::sku_3::']); + Price::query()->create(['sku' => '::sku_4::']); + + $this->artisan(UpdateAllPricesCommand::class); + + Bus::assertDispatchedTimes(UpdatePriceJob::class, 2); + } +} diff --git a/tests/Commands/Update/UpdatePriceCommandTest.php b/tests/Commands/Update/UpdatePriceCommandTest.php new file mode 100644 index 0000000..fa63f15 --- /dev/null +++ b/tests/Commands/Update/UpdatePriceCommandTest.php @@ -0,0 +1,38 @@ +create(['sku' => '::sku_1::']); + + $this->artisan(UpdatePriceCommand::class, [ + 'sku' => '::sku_1::', + ]); + + Bus::assertDispatched(UpdatePriceJob::class); + } + + #[Test] + public function it_throws_exception_on_missing_price(): void + { + $this->expectException(ModelNotFoundException::class); + + $this->artisan(UpdatePriceCommand::class, [ + 'sku' => '::some-non-existent-sku::', + ]); + } +} diff --git a/tests/Commands/Utility/ImportCustomerGroupsCommandTest.php b/tests/Commands/Utility/ImportCustomerGroupsCommandTest.php new file mode 100644 index 0000000..8775e21 --- /dev/null +++ b/tests/Commands/Utility/ImportCustomerGroupsCommandTest.php @@ -0,0 +1,22 @@ +artisan(ImportCustomerGroupsCommand::class); + + Bus::assertDispatched(ImportCustomerGroupsJob::class); + } +} diff --git a/tests/Commands/Utility/ProcessProductsWithMissingPricesCommandTest.php b/tests/Commands/Utility/ProcessProductsWithMissingPricesCommandTest.php new file mode 100644 index 0000000..36e387a --- /dev/null +++ b/tests/Commands/Utility/ProcessProductsWithMissingPricesCommandTest.php @@ -0,0 +1,22 @@ +artisan(ProcessProductsWithMissingPricesCommand::class); + + Bus::assertDispatched(ProcessProductsWithMissingPricesJob::class); + } +} diff --git a/tests/Data/DataObjectsTest.php b/tests/Data/DataObjectsTest.php deleted file mode 100644 index 01dd90e..0000000 --- a/tests/Data/DataObjectsTest.php +++ /dev/null @@ -1,176 +0,0 @@ -assertEquals($price, $data->getPrice()); - $this->assertEquals($storeId, $data->getStoreId()); - } - - public function test_base_price_price_parse(): void - { - $price = Money::of(10, - config('magento-prices.currency'), - new CustomContext(config('magento-prices.precision')), - config('magento-prices.rounding_mode') - ); - - $data = new BasePriceData($price, 0); - - $data->parsePrice(10); - - $this->assertEquals($price, $data->getPrice()); - } - - public function test_base_price_data_setters(): void - { - $price = Money::of(10, 'EUR'); - $storeId = 1; - - $data = new BasePriceData($price, $storeId); - - $price = $price->plus(10); - - $data->setPrice($price); - $data->setStoreId(10); - - $this->assertEquals($price, $data->getPrice()); - $this->assertEquals(10, $data->getStoreId()); - } - - public function test_base_price_to_array(): void - { - $price = Money::of(1.12, 'EUR'); - $storeId = 0; - - $data = new BasePriceData($price, $storeId); - - $this->assertEquals([ - 'storeId' => 0, - 'price' => 1.12, - ], $data->toArray()); - } - - public function test_tier_prices_constructor_order(): void - { - $storeId = 1; - $quantity = 1; - $groupId = '::group::'; - $priceType = 'fixed'; - $price = Money::of(10, 'EUR'); - - $data = new TierPriceData($groupId, $price, $quantity, $storeId, $priceType); - - $this->assertEquals($storeId, $data->getStoreId()); - $this->assertEquals($quantity, $data->getQuantity()); - $this->assertEquals($groupId, $data->getGroupId()); - $this->assertEquals($priceType, $data->getPriceType()); - $this->assertEquals($price, $data->getPrice()); - } - - public function test_tier_prices_default_values(): void - { - $storeId = 0; - $quantity = 1; - $groupId = '::group::'; - $priceType = 'fixed'; - $price = Money::of(10, 'EUR'); - - $data = new TierPriceData($groupId, $price); - - $this->assertEquals($storeId, $data->getStoreId()); - $this->assertEquals($quantity, $data->getQuantity()); - $this->assertEquals($groupId, $data->getGroupId()); - $this->assertEquals($priceType, $data->getPriceType()); - $this->assertEquals($price, $data->getPrice()); - } - - public function test_tier_prices_setters(): void - { - $groupId = '::group::'; - $price = Money::of(10, 'EUR'); - - $data = new TierPriceData($groupId, $price); - - $price = $price->plus(10); - - $data->setStoreId(10); - $data->setPrice($price); - $data->setQuantity(10); - $data->setGroupId('::new_group::'); - $data->setPriceType('::some_type::'); - - $this->assertEquals(10, $data->getStoreId()); - $this->assertEquals(10, $data->getQuantity()); - $this->assertEquals('::new_group::', $data->getGroupId()); - $this->assertEquals('::some_type::', $data->getPriceType()); - $this->assertEquals($price, $data->getPrice()); - } - - public function test_tier_prices_identifier(): void - { - $groupId = '::group::'; - $price = Money::of(10, 'EUR'); - - $data = new TierPriceData($groupId, $price); - - $this->assertEquals('0-1-::group::', $data->getIdentifier()); - } - - public function test_tier_prices_to_array(): void - { - $groupId = '::group::'; - $price = Money::of(10, 'EUR'); - - $data = new TierPriceData($groupId, $price); - - $this->assertEquals([ - 'storeId' => 0, - 'quantity' => 1, - 'price' => 10.00, - 'customer_group' => $groupId, - ], $data->toArray()); - } - - public function test_special_price(): void - { - $from = now()->subMonth(); - $to = now(); - - $price = Money::of(10, - config('magento-prices.currency'), - new CustomContext(config('magento-prices.precision')), - config('magento-prices.rounding_mode') - ); - - $specialPrice = new SpecialPriceData($price); - - $specialPrice->setPrice($price); - $specialPrice->setStoreId(1); - $specialPrice->setFrom($from); - $specialPrice->setTo($to); - $specialPrice->parsePrice(11); - - $this->assertEquals([ - 'storeId' => 1, - 'price' => '11.0000', - 'price_from' => $from->toDateTimeString(), - 'price_to' => $to->toDateTimeString(), - ], $specialPrice->toArray()); - } -} diff --git a/tests/Data/PriceDataTest.php b/tests/Data/PriceDataTest.php index 05b2ee8..b9e96e6 100644 --- a/tests/Data/PriceDataTest.php +++ b/tests/Data/PriceDataTest.php @@ -2,192 +2,53 @@ namespace JustBetter\MagentoPrices\Tests\Data; -use Brick\Money\Money; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Collection; -use JustBetter\MagentoPrices\Data\BasePriceData; +use Illuminate\Validation\ValidationException; use JustBetter\MagentoPrices\Data\PriceData; -use JustBetter\MagentoPrices\Data\TierPriceData; -use JustBetter\MagentoPrices\Models\MagentoPrice; use JustBetter\MagentoPrices\Tests\TestCase; +use PHPUnit\Framework\Attributes\Test; class PriceDataTest extends TestCase { - use RefreshDatabase; - - protected PriceData $data; - - protected function setUp(): void + #[Test] + public function it_passes_simple_rules(): void { - parent::setUp(); - - $basePrices = collect([ - new BasePriceData(Money::of(10, 'EUR'), 0), - new BasePriceData(Money::of(11, 'EUR'), 1), - new BasePriceData(Money::of(12, 'EUR'), 2), + PriceData::of([ + 'sku' => '::sku::', ]); - $tierPrices = collect([ - new TierPriceData('::group_1::', Money::of(10, 'EUR'), 0), - new TierPriceData('::group_1::', Money::of(8, 'EUR'), 10), - new TierPriceData('::group_2::', Money::of(5, 'EUR'), 0), - new TierPriceData('::group_2::', Money::of(2.5, 'EUR'), 10), - ]); - - $this->data = new PriceData('::sku::', $basePrices, $tierPrices); - } - - public function test_it_creates_model(): void - { - $this->assertDatabaseCount(MagentoPrice::class, 0); - - $this->data->getModel(); - - $this->assertDatabaseCount(MagentoPrice::class, 1); - } - - public function test_it_creates_model_once(): void - { - $this->assertDatabaseCount(MagentoPrice::class, 0); - - $this->data->getModel(); - $this->data->getModel(); - $this->data->getModel(); - - $this->assertDatabaseCount(MagentoPrice::class, 1); - } - - public function test_it_creates_array(): void - { - $this->assertEquals([ - 'base_prices' => collect([ - [ - 'storeId' => 0, - 'price' => 10.00, - ], - [ - 'storeId' => 1, - 'price' => 11.00, - ], - [ - 'storeId' => 2, - 'price' => 12.00, - ], - ]), - 'tier_prices' => collect([ - [ - 'storeId' => 0, - 'quantity' => 0, - 'price' => 10.00, - 'customer_group' => '::group_1::', - ], - [ - 'storeId' => 0, - 'quantity' => 10, - 'price' => 8.00, - 'customer_group' => '::group_1::', - ], - [ - 'storeId' => 0, - 'quantity' => 0, - 'price' => 5.00, - 'customer_group' => '::group_2::', - ], - [ - 'storeId' => 0, - 'quantity' => 10, - 'price' => 2.50, - 'customer_group' => '::group_2::', - ], - ]), - 'special_prices' => collect(), - ], $this->data->toArray()); + $this->assertTrue(true, 'No exception thrown'); } - public function test_it_gets_magento_base_prices(): void + #[Test] + public function it_fails_rules(): void { - $this->assertEquals([ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'store_id' => 0, - ], - [ - 'sku' => '::sku::', - 'price' => 11.0, - 'store_id' => 1, - ], - [ - 'sku' => '::sku::', - 'price' => 12.0, - 'store_id' => 2, - ], - ], $this->data->getMagentoBasePrices()); - } + $this->expectException(ValidationException::class); - public function test_it_gets_magento_tier_prices(): void - { - $this->assertEquals([ - [ - 'sku' => '::sku::', - 'price' => 10.0, - 'website_id' => 0, - 'quantity' => 1, - 'customer_group' => '::group_1::', - 'price_type' => 'fixed', - ], - [ - 'sku' => '::sku::', - 'price' => 8.0, - 'website_id' => 0, - 'quantity' => 10, - 'customer_group' => '::group_1::', - 'price_type' => 'fixed', - ], - [ - 'sku' => '::sku::', - 'price' => 5.0, - 'website_id' => 0, - 'quantity' => 1, - 'customer_group' => '::group_2::', - 'price_type' => 'fixed', - ], - [ - 'sku' => '::sku::', - 'price' => 2.5, - 'website_id' => 0, - 'quantity' => 10, - 'customer_group' => '::group_2::', - 'price_type' => 'fixed', - ], - ], $this->data->getMagentoTierPrices()); + PriceData::of([]); } - public function test_it_compares_base_price_data(): void + #[Test] + public function it_calculates_checksum(): void { - $newData = clone $this->data; - - $this->assertTrue($this->data->equals($newData)); - - /* @phpstan-ignore-next-line */ - $newData->basePrices = new Collection([ - new TierPriceData('::group_1::', Money::of(10, 'EUR'), 0), + $data = PriceData::of([ + 'sku' => '::sku::', ]); - $this->assertFalse($this->data->equals($newData)); + $this->assertEquals('b5a9aed3556af7b01952f7fdcd28fdd8', $data->checksum()); } - public function test_it_compares_base_tier_data(): void + #[Test] + public function it_handles_array_operations(): void { - $newData = clone $this->data; + $data = PriceData::of([ + 'sku' => '::sku::', + ]); - $this->assertTrue($this->data->equals($newData)); + $data['base_prices'] = []; - /* @phpstan-ignore-next-line */ - $newData->tierPrices = new Collection([ - new BasePriceData(Money::of(10, 'EUR'), 0), - ]); + $this->assertEquals([], $data['base_prices']); + unset($data['base_prices']); - $this->assertFalse($this->data->equals($newData)); + $this->assertNull($data['base_prices']); } } diff --git a/tests/Fakes/FakeNullRepository.php b/tests/Fakes/FakeNullRepository.php new file mode 100644 index 0000000..9bbc2fa --- /dev/null +++ b/tests/Fakes/FakeNullRepository.php @@ -0,0 +1,14 @@ + $sku, + 'base_prices' => [ + [ + 'store_id' => 0, + 'price' => 10, + ], + [ + 'store_id' => 2, + 'price' => 19, + ], + ], + 'tier_prices' => [ + [ + 'website_id' => 0, + 'customer_group' => 'group_1', + 'price_type' => 'fixed', + 'quantity' => 1, + 'price' => 8, + ], + [ + 'website_id' => 0, + 'customer_group' => '4040', + 'price_type' => 'group_2', + 'quantity' => 1, + 'price' => 7, + ], + ], + 'special_prices' => [ + [ + 'store_id' => 0, + 'price' => 5, + 'price_from' => now()->subWeek()->toDateString(), + 'price_to' => now()->addWeek()->toDateString(), + ], + ], + ]); + } +} diff --git a/tests/Feature/RetrievePricesJobTest.php b/tests/Feature/RetrievePricesJobTest.php deleted file mode 100644 index cdab2e8..0000000 --- a/tests/Feature/RetrievePricesJobTest.php +++ /dev/null @@ -1,40 +0,0 @@ -mock(ChecksMagentoExistence::class, function (MockInterface $mock) { - $mock->shouldReceive('exists') - ->andReturnTrue(); - }); - } - - public function test_it_retrieves_all(): void - { - $this->artisan(RetrievePricesCommand::class); - - $prices = MagentoPrice::all(); - - $this->assertCount(2, $prices); - } - - public function test_it_retrieves_by_date(): void - { - $this->artisan(RetrievePricesCommand::class, ['--date' => 'today']); - - $prices = MagentoPrice::all(); - - $this->assertCount(1, $prices); - } -} diff --git a/tests/Feature/UpdatePricesJobTest.php b/tests/Feature/UpdatePricesJobTest.php deleted file mode 100644 index 2d94150..0000000 --- a/tests/Feature/UpdatePricesJobTest.php +++ /dev/null @@ -1,44 +0,0 @@ -mock(ChecksMagentoExistence::class, function (MockInterface $mock) { - $mock->shouldReceive('exists') - ->andReturnTrue(); - }); - - $this->artisan(RetrievePricesCommand::class); - } - - public function test_it_updates(): void - { - Bus::fake([UpdateMagentoBasePricesJob::class]); - - $this->artisan(UpdatePriceCommand::class, ['sku' => '123']); - - Bus::assertBatched(function (PendingBatch $batch) { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoBasePricesJob) { - return true; - } - } - - return false; - }); - } -} diff --git a/tests/Helpers/MoneyHelperTest.php b/tests/Helpers/MoneyHelperTest.php deleted file mode 100644 index 5afca2a..0000000 --- a/tests/Helpers/MoneyHelperTest.php +++ /dev/null @@ -1,49 +0,0 @@ -set('laravel-magento-prices.currency', 'EUR'); - config()->set('laravel-magento-prices.precision', 4); - config()->set('laravel-magento-prices.rounding_mode', RoundingMode::HALF_UP); - - /** @var MoneyHelper $helper */ - $helper = app(MoneyHelper::class); - - $money = $helper->getMoney($amount, $method); - - $this->assertEquals($expectedAmount ?? $amount, $money->getAmount()->toFloat()); - } - - public static function provider(): array - { - return [ - [ - 'amount' => 10, - 'method' => 'of', - ], - [ - 'amount' => 1000, - 'method' => 'ofMinor', - 'expectedAmount' => 10, - ], - [ - 'amount' => 1.1234, - 'method' => 'of', - ], - [ - 'amount' => 112, - 'method' => 'ofMinor', - 'expectedAmount' => 1.12, - ], - ]; - } -} diff --git a/tests/Jobs/ImportCustomerGroupsJobTest.php b/tests/Jobs/ImportCustomerGroupsJobTest.php deleted file mode 100644 index 98f26d1..0000000 --- a/tests/Jobs/ImportCustomerGroupsJobTest.php +++ /dev/null @@ -1,23 +0,0 @@ -mock(ImportsCustomerGroups::class, function (MockInterface $mock): void { - $mock - ->shouldReceive('import') - ->once(); - }); - - ImportCustomerGroupsJob::dispatch(); - } -} diff --git a/tests/Jobs/MonitorWaitTimesJobTest.php b/tests/Jobs/MonitorWaitTimesJobTest.php deleted file mode 100644 index 414ed9d..0000000 --- a/tests/Jobs/MonitorWaitTimesJobTest.php +++ /dev/null @@ -1,20 +0,0 @@ -mock(MonitorsWaitTimes::class, function (MockInterface $mock) { - $mock->shouldReceive('monitor')->once(); - }); - - MonitorWaitTimesJob::dispatchSync(); - } -} diff --git a/tests/Jobs/ProcessPriceJobTest.php b/tests/Jobs/ProcessPriceJobTest.php deleted file mode 100644 index a319396..0000000 --- a/tests/Jobs/ProcessPriceJobTest.php +++ /dev/null @@ -1,29 +0,0 @@ -mock(ProcessesPrice::class, function (MockInterface $mock) { - $mock->shouldReceive('process')->once(); - }); - - ProcessPriceJob::dispatchSync(new PriceData('::sku::', collect())); - } - - public function test_queue_attributes(): void - { - $job = new ProcessPriceJob(new PriceData('::sku::', collect())); - - $this->assertEquals('::sku::', $job->uniqueId()); - $this->assertEquals(['::sku::'], $job->tags()); - } -} diff --git a/tests/Jobs/ProcessPricesJobTest.php b/tests/Jobs/ProcessPricesJobTest.php new file mode 100644 index 0000000..3d66522 --- /dev/null +++ b/tests/Jobs/ProcessPricesJobTest.php @@ -0,0 +1,22 @@ +mock(ProcessesPrices::class, function (MockInterface $mock): void { + $mock->shouldReceive('process')->once(); + }); + + ProcessPricesJob::dispatch(); + } +} diff --git a/tests/Jobs/Retrieval/RetrieveAllPricesJobTest.php b/tests/Jobs/Retrieval/RetrieveAllPricesJobTest.php new file mode 100644 index 0000000..af9df8e --- /dev/null +++ b/tests/Jobs/Retrieval/RetrieveAllPricesJobTest.php @@ -0,0 +1,38 @@ +mock(RetrievesPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('retrieve')->once(); + }); + + RetrievePriceJob::dispatch('::sku::', false); + } + + #[Test] + public function it_has_unique_id(): void + { + $job = new RetrievePriceJob('::sku::', false); + + $this->assertEquals('::sku::', $job->uniqueId()); + } + + #[Test] + public function it_has_tags(): void + { + $job = new RetrievePriceJob('::sku::', false); + + $this->assertEquals(['::sku::'], $job->tags()); + } +} diff --git a/tests/Jobs/Retrieval/RetrievePriceJobTest.php b/tests/Jobs/Retrieval/RetrievePriceJobTest.php new file mode 100644 index 0000000..2da980a --- /dev/null +++ b/tests/Jobs/Retrieval/RetrievePriceJobTest.php @@ -0,0 +1,22 @@ +mock(RetrievesAllPrices::class, function (MockInterface $mock): void { + $mock->shouldReceive('retrieve')->once(); + }); + + RetrieveAllPricesJob::dispatch(); + } +} diff --git a/tests/Jobs/Retrieval/SavePriceJobTest.php b/tests/Jobs/Retrieval/SavePriceJobTest.php new file mode 100644 index 0000000..9a5deb4 --- /dev/null +++ b/tests/Jobs/Retrieval/SavePriceJobTest.php @@ -0,0 +1,45 @@ +mock(SavesPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('save')->once(); + }); + + $priceData = PriceData::of(['sku' => '::sku::']); + + SavePriceJob::dispatch($priceData, false); + } + + #[Test] + public function it_has_unique_id(): void + { + $priceData = PriceData::of(['sku' => '::sku::']); + + $job = new SavePriceJob($priceData, false); + + $this->assertEquals('::sku::', $job->uniqueId()); + } + + #[Test] + public function it_has_tags(): void + { + $priceData = PriceData::of(['sku' => '::sku::']); + + $job = new SavePriceJob($priceData, false); + + $this->assertEquals(['::sku::'], $job->tags()); + } +} diff --git a/tests/Jobs/RetrievePriceJobTest.php b/tests/Jobs/RetrievePriceJobTest.php deleted file mode 100644 index 6e85055..0000000 --- a/tests/Jobs/RetrievePriceJobTest.php +++ /dev/null @@ -1,60 +0,0 @@ -set('magento-prices.retrievers.price', DummyPriceRetriever::class); - - $this->mock(DummyPriceRetriever::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieve') - ->andReturn(new PriceData('::sku::', collect())); - }); - - RetrievePriceJob::dispatchSync('::sku::'); - - Bus::assertDispatched(ProcessPriceJob::class); - } - - public function test_it_does_not_dispatch_process_job(): void - { - Bus::fake([ProcessPriceJob::class]); - - config()->set('magento-prices.retrievers.price', DummyPriceRetriever::class); - - $this->mock(DummyPriceRetriever::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieve') - ->andReturnNull(); - }); - - RetrievePriceJob::dispatchSync('::sku::'); - - Bus::assertNotDispatched(ProcessPriceJob::class); - } - - public function test_queue_attributes_force_false(): void - { - $job = new RetrievePriceJob('::sku::', false); - - $this->assertEquals(['::sku::', 'force:false'], $job->tags()); - } - - public function test_queue_attributes_force_true(): void - { - $job = new RetrievePriceJob('::sku::', true); - - $this->assertEquals(['::sku::', 'force:true'], $job->tags()); - } -} diff --git a/tests/Jobs/RetrievePricesJobTest.php b/tests/Jobs/RetrievePricesJobTest.php deleted file mode 100644 index c47faa8..0000000 --- a/tests/Jobs/RetrievePricesJobTest.php +++ /dev/null @@ -1,61 +0,0 @@ -set('magento-prices.retrievers.sku', DummySkuRetriever::class); - - $this->mock(DummySkuRetriever::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieveAll') - ->andReturn(collect(['::sku_1::', '::sku_2::'])); - }); - - RetrievePricesJob::dispatchSync(); - - Bus::assertDispatchedTimes(RetrievePriceJob::class, 2); - } - - public function test_it_dispatches_date_retrieve_job(): void - { - Bus::fake([RetrievePriceJob::class]); - - config()->set('magento-prices.retrievers.sku', DummySkuRetriever::class); - - $this->mock(DummySkuRetriever::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieveByDate') - ->andReturn(collect()); - }); - - RetrievePricesJob::dispatchSync(now()); - - Bus::assertDispatchedTimes(RetrievePriceJob::class, 0); - } - - public function test_queue_attributes_force_false(): void - { - $job = new RetrievePricesJob(null, false); - - $this->assertEquals(['force:false', 'all'], $job->tags()); - } - - public function test_queue_attributes_force_true(): void - { - $from = now(); - - $job = new RetrievePricesJob($from, true); - - $this->assertEquals(['force:true', 'from:'.$from->toDateTimeString()], $job->tags()); - } -} diff --git a/tests/Jobs/SyncMissingPricesJobTest.php b/tests/Jobs/SyncMissingPricesJobTest.php deleted file mode 100644 index 70e358e..0000000 --- a/tests/Jobs/SyncMissingPricesJobTest.php +++ /dev/null @@ -1,38 +0,0 @@ -create(['sku' => '::sku_1::']); - - $this->mock(FindsProductsWithMissingPrices::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieve') - ->andReturn(collect(['::sku_1::', '::sku_2::'])); - }); - - SyncMissingPricesJob::dispatchSync(); - - Bus::assertDispatched(UpdatePriceJob::class, function (UpdatePriceJob $job) { - return $job->sku === '::sku_1::'; - }); - - Bus::assertDispatched(RetrievePriceJob::class, function (RetrievePriceJob $job) { - return $job->sku === '::sku_2::'; - }); - } -} diff --git a/tests/Jobs/SyncPricesJobTest.php b/tests/Jobs/SyncPricesJobTest.php deleted file mode 100644 index 5690aa9..0000000 --- a/tests/Jobs/SyncPricesJobTest.php +++ /dev/null @@ -1,20 +0,0 @@ -mock(SyncsPrices::class, function (MockInterface $mock) { - $mock->shouldReceive('sync')->once(); - }); - - SyncPricesJob::dispatchSync(); - } -} diff --git a/tests/Jobs/Update/UpdatePriceJobTest.php b/tests/Jobs/Update/UpdatePriceJobTest.php new file mode 100644 index 0000000..d95199c --- /dev/null +++ b/tests/Jobs/Update/UpdatePriceJobTest.php @@ -0,0 +1,48 @@ +mock(UpdatesPrice::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->once(); + }); + + /** @var Price $price */ + $price = Price::query()->create(['sku' => '::sku::']); + + UpdatePriceJob::dispatch($price); + } + + #[Test] + public function it_has_unique_id(): void + { + /** @var Price $price */ + $price = Price::query()->create(['sku' => '::sku::']); + + $job = new UpdatePriceJob($price); + + $this->assertEquals($price->id, $job->uniqueId()); + } + + #[Test] + public function it_has_tags(): void + { + /** @var Price $price */ + $price = Price::query()->create(['sku' => '::sku::']); + + $job = new UpdatePriceJob($price); + + $this->assertEquals(['::sku::'], $job->tags()); + } +} diff --git a/tests/Jobs/Update/UpdatePricesAsyncJobTest.php b/tests/Jobs/Update/UpdatePricesAsyncJobTest.php new file mode 100644 index 0000000..be7b28e --- /dev/null +++ b/tests/Jobs/Update/UpdatePricesAsyncJobTest.php @@ -0,0 +1,35 @@ +mock(UpdatesPricesAsync::class, function (MockInterface $mock): void { + $mock->shouldReceive('update')->once(); + }); + + UpdatePricesAsyncJob::dispatch(collect()); + } + + #[Test] + public function it_has_tags(): void + { + $prices = collect([ + Price::query()->create(['sku' => '::sku_1::']), + Price::query()->create(['sku' => '::sku_2::']), + ]); + $job = new UpdatePricesAsyncJob($prices); + + $this->assertEquals(['::sku_1::', '::sku_2::'], $job->tags()); + } +} diff --git a/tests/Jobs/UpdateMagentoBasePricesJobTest.php b/tests/Jobs/UpdateMagentoBasePricesJobTest.php deleted file mode 100644 index d595668..0000000 --- a/tests/Jobs/UpdateMagentoBasePricesJobTest.php +++ /dev/null @@ -1,45 +0,0 @@ -mock(UpdatesMagentoBasePrice::class, function (MockInterface $mock) { - $mock->shouldReceive('update')->once(); - }); - - UpdateMagentoBasePricesJob::dispatchSync(new PriceData('::sku::', collect())); - } - - public function test_queue_attributes(): void - { - $job = new UpdateMagentoBasePricesJob(new PriceData('::sku::', collect())); - - $this->assertEquals('::sku::', $job->uniqueId()); - $this->assertEquals(['::sku::'], $job->tags()); - } - - public function test_it_handles_failure(): void - { - $job = new UpdateMagentoBasePricesJob(new PriceData('::sku::', collect())); - - $exception = (new Response(new \GuzzleHttp\Psr7\Response(500)))->toException(); - - $job->failed($exception); - - $model = MagentoPrice::findBySku('::sku::'); - - $this->assertEquals(1, $model->fail_count); - $this->assertNotNull($model->last_failed); - } -} diff --git a/tests/Jobs/UpdateMagentoSpecialPricesJobTest.php b/tests/Jobs/UpdateMagentoSpecialPricesJobTest.php deleted file mode 100644 index 95876ea..0000000 --- a/tests/Jobs/UpdateMagentoSpecialPricesJobTest.php +++ /dev/null @@ -1,45 +0,0 @@ -mock(UpdatesMagentoSpecialPrice::class, function (MockInterface $mock) { - $mock->shouldReceive('update')->once(); - }); - - UpdateMagentoSpecialPricesJob::dispatchSync(new PriceData('::sku::', collect())); - } - - public function test_queue_attributes(): void - { - $job = new UpdateMagentoSpecialPricesJob(new PriceData('::sku::', collect())); - - $this->assertEquals('::sku::', $job->uniqueId()); - $this->assertEquals(['::sku::'], $job->tags()); - } - - public function test_it_handles_failure(): void - { - $job = new UpdateMagentoSpecialPricesJob(new PriceData('::sku::', collect())); - - $exception = (new Response(new \GuzzleHttp\Psr7\Response(500)))->toException(); - - $job->failed($exception); - - $model = MagentoPrice::findBySku('::sku::'); - - $this->assertEquals(1, $model->fail_count); - $this->assertNotNull($model->last_failed); - } -} diff --git a/tests/Jobs/UpdateMagentoTierPricesJobTest.php b/tests/Jobs/UpdateMagentoTierPricesJobTest.php deleted file mode 100644 index 1ff8656..0000000 --- a/tests/Jobs/UpdateMagentoTierPricesJobTest.php +++ /dev/null @@ -1,45 +0,0 @@ -mock(UpdatesMagentoTierPrice::class, function (MockInterface $mock) { - $mock->shouldReceive('update')->once(); - }); - - UpdateMagentoTierPricesJob::dispatchSync(new PriceData('::sku::', collect())); - } - - public function test_queue_attributes(): void - { - $job = new UpdateMagentoTierPricesJob(new PriceData('::sku::', collect())); - - $this->assertEquals('::sku::', $job->uniqueId()); - $this->assertEquals(['::sku::'], $job->tags()); - } - - public function test_it_handles_failure(): void - { - $job = new UpdateMagentoTierPricesJob(new PriceData('::sku::', collect())); - - $exception = (new Response(new \GuzzleHttp\Psr7\Response(500)))->toException(); - - $job->failed($exception); - - $model = MagentoPrice::findBySku('::sku::'); - - $this->assertEquals(1, $model->fail_count); - $this->assertNotNull($model->last_failed); - } -} diff --git a/tests/Jobs/UpdatePriceJobTest.php b/tests/Jobs/UpdatePriceJobTest.php deleted file mode 100644 index eda1afd..0000000 --- a/tests/Jobs/UpdatePriceJobTest.php +++ /dev/null @@ -1,278 +0,0 @@ -mock(ChecksMagentoExistence::class, function (MockInterface $mock) { - $mock->shouldReceive('exists') - ->with('::sku::') - ->andReturnTrue(); - }); - - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'base_prices' => [['price' => 10, 'storeId' => 0]], - 'tier_prices' => [['price' => 10, 'storeId' => 0, 'customer_group' => 'ALL GROUPS', 'quantity' => 1]], - 'special_prices' => [['price' => 10, 'storeId' => 0, 'price_from' => null, 'price_to' => null]], - ]); - } - - public function test_it_checks_existence(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku_2::', - ]); - - $this->mock(ChecksMagentoExistence::class, function (MockInterface $mock) { - $mock->shouldReceive('exists') - ->with('::sku_2::') - ->once() - ->andReturnFalse(); - }); - - UpdatePriceJob::dispatchSync('::sku_2::'); - - $model = MagentoPrice::findBySku('::sku_2::'); - - $this->assertFalse($model->update); - $this->assertFalse($model->sync); - } - - public function test_it_dispatches_update_jobs(): void - { - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - /** @var Collection $jobs */ - $jobs = $batch->jobs->map(function (mixed $job): string { - return get_class($job); - }); - - if ($jobs->contains('JustBetter\MagentoPrices\Jobs\UpdateMagentoBasePricesJob') && $jobs->contains('JustBetter\MagentoPrices\Jobs\UpdateMagentoTierPricesJob') && $jobs->contains('JustBetter\MagentoPrices\Jobs\UpdateMagentoSpecialPricesJob')) { - return true; - } - - return false; - }); - - $model = MagentoPrice::findBySku('::sku::'); - - $this->assertFalse($model->update); - } - - public function test_it_dispatches_updated_event(): void - { - UpdatePriceJob::dispatchSync('::sku::'); - - Event::assertDispatched(UpdatedPriceEvent::class); - } - - /** @dataProvider jobTypes */ - public function test_it_dispatches_update_jobs_by_type(string $type, string $dispatch, array $notDispatch): void - { - UpdatePriceJob::dispatchSync('::sku::', $type); - - Bus::assertBatched(function (PendingBatch $batch) use ($dispatch): bool { - return $batch->jobs->first() instanceof $dispatch; - }); - - Bus::assertBatched(function (PendingBatch $batch) use ($notDispatch): bool { - foreach ($notDispatch as $notDispatchJobClass) { - foreach ($batch->jobs as $job) { - if ($job instanceof $notDispatchJobClass) { - return false; - } - } - } - - return true; - }); - } - - public static function jobTypes(): array - { - return [ - [ - 'type' => 'base', - 'dispatch' => UpdateMagentoBasePricesJob::class, - 'notDispatch' => [UpdateMagentoTierPricesJob::class, UpdateMagentoSpecialPricesJob::class], - ], - [ - 'type' => 'tier', - 'dispatch' => UpdateMagentoTierPricesJob::class, - 'notDispatch' => [UpdateMagentoBasePricesJob::class, UpdateMagentoSpecialPricesJob::class], - ], - [ - 'type' => 'special', - 'dispatch' => UpdateMagentoSpecialPricesJob::class, - 'notDispatch' => [UpdateMagentoBasePricesJob::class, UpdateMagentoTierPricesJob::class], - ], - ]; - } - - public function test_it_does_not_dispatch_empty_base(): void - { - MagentoPrice::findBySku('::sku::') - ->update(['base_prices' => []]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoBasePricesJob) { - return false; - } - } - - return true; - }); - } - - public function test_it_does_not_dispatch_empty_tier(): void - { - MagentoPrice::findBySku('::sku::') - ->update(['tier_prices' => []]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoTierPricesJob) { - return false; - } - } - - return true; - }); - } - - public function test_it_does_not_dispatch_empty_special(): void - { - MagentoPrice::findBySku('::sku::') - ->update(['special_prices' => []]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoSpecialPricesJob) { - return false; - } - } - - return true; - }); - } - - public function test_it_does_dispatch_empty_tier_to_remove_tier_prices(): void - { - MagentoPrice::findBySku('::sku::') - ->update(['tier_prices' => [], 'has_tier' => true]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoTierPricesJob) { - return true; - } - } - - return false; - }); - - $model = MagentoPrice::findBySku('::sku::'); - $this->assertFalse($model->has_tier); - } - - public function test_it_does_dispatch_empty_special_to_remove_special_prices(): void - { - MagentoPrice::findBySku('::sku::') - ->update(['special_prices' => [], 'has_special' => true]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch): bool { - foreach ($batch->jobs as $job) { - if ($job instanceof UpdateMagentoSpecialPricesJob) { - return true; - } - } - - return false; - }); - - $model = MagentoPrice::findBySku('::sku::'); - $this->assertFalse($model->has_special); - } - - public function test_queue_attributes(): void - { - $job = new UpdatePriceJob('::sku::'); - - $this->assertEquals('::sku::', $job->uniqueId()); - $this->assertEquals(['::sku::', 'type:all'], $job->tags()); - - $job = new UpdatePriceJob('::sku::', 'base'); - $this->assertEquals(['::sku::', 'type:base'], $job->tags()); - } - - public function test_it_does_nothing(): void - { - $this->mock(ChecksMagentoExistence::class, function (MockInterface $mock) { - $mock->shouldNotReceive('exists'); - }); - - UpdatePriceJob::dispatch('::non_existent::'); - } - - public function test_it_clears_failure_data_if_success(): void - { - MagentoPrice::findBySku('::sku::') - ->update([ - 'fail_count' => 5, - 'last_failed' => now(), - ]); - - UpdatePriceJob::dispatchSync('::sku::'); - - Bus::assertBatched(function (PendingBatch $batch) { - /* @var \Illuminate\Queue\SerializableClosure $thenCallback */ - [$thenCallback] = $batch->thenCallbacks(); - - $thenCallback->getClosure()->call($this); - - $model = MagentoPrice::findBySku('::sku::'); - - $this->assertEquals(0, $model->fail_count); - $this->assertNull($model->last_failed); - - return true; - }); - } -} diff --git a/tests/Jobs/Utility/ImportCustomerGroupsJobTest.php b/tests/Jobs/Utility/ImportCustomerGroupsJobTest.php new file mode 100644 index 0000000..0594f19 --- /dev/null +++ b/tests/Jobs/Utility/ImportCustomerGroupsJobTest.php @@ -0,0 +1,22 @@ +mock(ImportsCustomerGroups::class, function (MockInterface $mock): void { + $mock->shouldReceive('import')->once(); + }); + + ImportCustomerGroupsJob::dispatch(); + } +} diff --git a/tests/Jobs/Utility/ProcessProductsWithMissingPricesJobTest.php b/tests/Jobs/Utility/ProcessProductsWithMissingPricesJobTest.php new file mode 100644 index 0000000..d6466c2 --- /dev/null +++ b/tests/Jobs/Utility/ProcessProductsWithMissingPricesJobTest.php @@ -0,0 +1,22 @@ +mock(ProcessesProductsWithMissingPrices::class, function (MockInterface $mock): void { + $mock->shouldReceive('process')->once(); + }); + + ProcessProductsWithMissingPricesJob::dispatch(); + } +} diff --git a/tests/Listeners/BulkOperationStatusListenerTest.php b/tests/Listeners/BulkOperationStatusListenerTest.php new file mode 100644 index 0000000..71573af --- /dev/null +++ b/tests/Listeners/BulkOperationStatusListenerTest.php @@ -0,0 +1,90 @@ +create([ + 'sku' => 'sku', + ]); + + /** @var BulkRequest $request */ + $request = BulkRequest::query()->create([ + 'magento_connection' => 'default', + 'store_code' => 'store', + 'path' => 'products', + 'bulk_uuid' => '::uuid::', + 'request' => [], + 'response' => [], + ]); + + /** @var BulkOperation $operation */ + $operation = $request->operations()->create([ + 'subject_type' => get_class($model), + 'subject_id' => $model->getKey(), + 'operation_id' => 0, + 'status' => OperationStatus::Complete, + ]); + + /** @var BulkOperationStatusListener $listener */ + $listener = app(BulkOperationStatusListener::class); + + $listener->execute($operation); + + Event::assertDispatched(UpdatedPriceEvent::class); + $this->assertNotNull($model->refresh()->last_updated); + } + + #[Test] + public function it_handles_failed_status(): void + { + Event::fake(); + + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => 'sku', + ]); + + /** @var BulkRequest $request */ + $request = BulkRequest::query()->create([ + 'magento_connection' => 'default', + 'store_code' => 'store', + 'path' => 'products', + 'bulk_uuid' => '::uuid::', + 'request' => [], + 'response' => [], + ]); + + /** @var BulkOperation $operation */ + $operation = $request->operations()->create([ + 'subject_type' => get_class($model), + 'subject_id' => $model->getKey(), + 'operation_id' => 0, + 'status' => OperationStatus::NotRetriablyFailed, + ]); + + /** @var BulkOperationStatusListener $listener */ + $listener = app(BulkOperationStatusListener::class); + + $listener->execute($operation); + + Event::assertNotDispatched(UpdatedPriceEvent::class); + $this->assertNull($model->refresh()->last_updated); + } +} diff --git a/tests/Mocks/CheckMagentoExistenceMock.php b/tests/Mocks/CheckMagentoExistenceMock.php deleted file mode 100644 index ee810c7..0000000 --- a/tests/Mocks/CheckMagentoExistenceMock.php +++ /dev/null @@ -1,13 +0,0 @@ -setAttribute('special_prices', $original); - - $model->syncOriginal(); - - $model->setAttribute('special_prices', $updated); - - $this->assertEquals($expectChanged, $model->specialPriceChanged()); - } - - public static function specialPriceProvider(): array - { - return [ - 'No change' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'expected_change' => false, - ], - - 'Store id changed' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 0, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'expected_change' => true, - ], - - 'Price changed' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '26.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'expected_change' => true, - ], - - 'Date updated 1 day' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-23 00:00:00', - 'price_from' => '2022-06-23 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'expected_change' => false, - ], - - 'Date expired' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2022-06-23 00:00:00', - 'price_from' => '2021-06-23 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'expected_change' => true, - ], - - 'Original empty' => [ - 'original' => [], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'expected_change' => true, - ], - - 'Updated empty' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'updated' => [], - 'expected_change' => true, - ], - - 'Multiple one changed' => [ - 'original' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - [ - 'price' => '26.0000', - 'storeId' => 2, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'updated' => [ - [ - 'price' => '25.0000', - 'storeId' => 1, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - [ - 'price' => '24.0000', - 'storeId' => 2, - 'price_to' => '2023-06-24 00:00:00', - 'price_from' => '2022-06-24 00:00:00', - ], - ], - 'expected_change' => true, - ], - ]; - } - - public function test_it_gets_base_prices(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'base_prices' => [['price' => 10, 'storeId' => 1]], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $basePrices = $model->base_prices; - /** @var BasePriceData $basePrice */ - $basePrice = $basePrices->first(); - - $this->assertCount(1, $basePrices); - $this->assertEquals(1, $basePrice->storeId); - $this->assertEquals(10, $basePrice->price->getAmount()->toInt()); - } - - public function test_it_gets_tier_prices(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'tier_prices' => [['price' => 10, 'storeId' => 1, 'customer_group' => 'ALL GROUPS', 'quantity' => 1]], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $tierPrices = $model->tier_prices; - /** @var TierPriceData $tierPrice */ - $tierPrice = $tierPrices->first(); - - $this->assertCount(1, $tierPrices); - $this->assertEquals(1, $tierPrice->storeId); - $this->assertEquals(1, $tierPrice->quantity); - $this->assertEquals('ALL GROUPS', $tierPrice->groupId); - $this->assertEquals(10, $tierPrice->price->getAmount()->toInt()); - } - - public function test_it_gets_special_prices(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'special_prices' => [['price' => 10, 'storeId' => 1, 'price_from' => null, 'price_to' => null]], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $specialPrices = $model->special_prices; - /** @var SpecialPriceData $specialPrice */ - $specialPrice = $specialPrices->first(); - - $this->assertCount(1, $specialPrices); - $this->assertEquals(1, $specialPrice->storeId); - $this->assertEquals(10, $specialPrice->price->getAmount()->toInt()); - } - - public function test_it_gets_single_base_price(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'base_prices' => ['price' => 10, 'storeId' => 1], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $basePrices = $model->base_prices; - /** @var BasePriceData $basePrice */ - $basePrice = $basePrices->first(); - - $this->assertCount(1, $basePrices); - $this->assertEquals(1, $basePrice->storeId); - $this->assertEquals(10, $basePrice->price->getAmount()->toInt()); - } - - public function test_it_gets_single_tier_price(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'tier_prices' => ['price' => 10, 'storeId' => 1, 'customer_group' => 'ALL GROUPS', 'quantity' => 1], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $tierPrices = $model->tier_prices; - /** @var TierPriceData $tierPrice */ - $tierPrice = $tierPrices->first(); - - $this->assertCount(1, $tierPrices); - $this->assertEquals(1, $tierPrice->storeId); - $this->assertEquals(1, $tierPrice->quantity); - $this->assertEquals('ALL GROUPS', $tierPrice->groupId); - $this->assertEquals(10, $tierPrice->price->getAmount()->toInt()); - } - - public function test_it_gets_single_special_price(): void - { - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'special_prices' => ['price' => 10, 'storeId' => 1, 'price_from' => null, 'price_to' => null], - ]); - - $model = MagentoPrice::findBySku('::sku::'); - - $specialPrices = $model->special_prices; - /** @var SpecialPriceData $specialPrice */ - $specialPrice = $specialPrices->first(); - - $this->assertCount(1, $specialPrices); - $this->assertEquals(1, $specialPrice->storeId); - $this->assertEquals(10, $specialPrice->price->getAmount()->toInt()); - } - - public function test_it_fails_when_registering_too_much_errors(): void - { - config()->set('magento-prices.fail_count', 3); - MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'fail_count' => 3, - ]); - - $model = MagentoPrice::findBySku('::sku::'); - $model->registerError(); - - $this->assertFalse($model->update); - $this->assertFalse($model->retrieve); - $this->assertEquals(0, $model->fail_count); - } - - public function test_it_has_activity(): void - { - /** @var MagentoPrice $price */ - $price = MagentoPrice::query()->create([ - 'sku' => '::sku::', - 'fail_count' => 3, - ]); - - $binds = $price->activity()->getBindings(); - $this->assertEquals([MagentoPrice::class, $price->id], $binds); - } -} diff --git a/tests/Models/PriceModelTest.php b/tests/Models/PriceModelTest.php new file mode 100644 index 0000000..81ded77 --- /dev/null +++ b/tests/Models/PriceModelTest.php @@ -0,0 +1,89 @@ +create([ + 'sku' => '::sku::', + 'update' => true, + ]); + + $model->registerFailure(); + + $this->assertNotNull($model->last_failed); + $this->assertEquals(1, $model->fail_count); + $this->assertTrue($model->update); + } + + #[Test] + public function it_will_set_retrieve_update_too_many_failures(): void + { + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'fail_count' => 100, + 'retrieve' => true, + 'update' => true, + ]); + + $model->registerFailure(); + + $this->assertEquals(0, $model->fail_count); + $this->assertFalse($model->retrieve); + $this->assertFalse($model->update); + } + + #[Test] + public function it_resets_double_state_update(): void + { + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'retrieve' => false, + 'update' => false, + ]); + + $model->retrieve = true; + $model->update = true; + + $model->save(); + + $this->assertTrue($model->retrieve); + $this->assertFalse($model->update); + } + + #[Test] + public function it_resets_double_state_retrieve(): void + { + /** @var Price $model */ + $model = Price::query()->create([ + 'sku' => '::sku::', + 'retrieve' => false, + 'update' => false, + ]); + + $model->retrieve = true; + $model->update = true; + + $model->save(); + + $this->assertTrue($model->retrieve); + $this->assertFalse($model->update); + + $model->update = true; + + $model->save(); + + $this->assertFalse($model->retrieve); + $this->assertTrue($model->update); + } +} diff --git a/tests/Repository/RepositoryTest.php b/tests/Repository/RepositoryTest.php new file mode 100644 index 0000000..a0f085f --- /dev/null +++ b/tests/Repository/RepositoryTest.php @@ -0,0 +1,57 @@ +assertEquals(250, $repository->retrieveLimit()); + $this->assertEquals(250, $repository->updateLimit()); + $this->assertEquals(3, $repository->failLimit()); + } + + #[Test] + public function it_resolves_repository(): void + { + config()->set('magento-prices.repository', FakeRepository::class); + + $resolved = BaseRepository::resolve(); + + $this->assertInstanceOf(FakeRepository::class, $resolved); + } + + #[Test] + public function it_throws_exception(): void + { + $repository = BaseRepository::resolve(); + + $this->expectException(NotImplementedException::class); + + $repository->retrieve('::sku::'); + } + + #[Test] + public function it_retrieve_magento_skus(): void + { + MagentoProduct::query()->create(['sku' => '::sku_1::', 'exists_in_magento' => true]); + MagentoProduct::query()->create(['sku' => '::sku_2::', 'exists_in_magento' => false]); + MagentoProduct::query()->create(['sku' => '::sku_3::', 'exists_in_magento' => true]); + + $repository = BaseRepository::resolve(); + + $this->assertEquals(['::sku_1::', '::sku_3::'], $repository->skus()->toArray()); + } +} diff --git a/tests/Retriever/DummyRetrieversTest.php b/tests/Retriever/DummyRetrieversTest.php deleted file mode 100644 index c56040e..0000000 --- a/tests/Retriever/DummyRetrieversTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertEquals(['123', '456'], $retriever->retrieveAll()->toArray()); - $this->assertEquals(['789'], $retriever->retrieveByDate(now())->toArray()); - } - - public function test_dummy_price_retriever(): void - { - $retriever = new DummyPriceRetriever(); - - $price = $retriever->retrieve('::sku::'); - - $this->assertEquals('::sku::', $price->sku); - $this->assertCount(1, $price->basePrices); - $this->assertCount(0, $price->tierPrices); - $this->assertCount(0, $price->specialPrices); - } -} diff --git a/tests/Retriever/SkuRetrieverTest.php b/tests/Retriever/SkuRetrieverTest.php deleted file mode 100644 index 6486458..0000000 --- a/tests/Retriever/SkuRetrieverTest.php +++ /dev/null @@ -1,24 +0,0 @@ -assertCount(0, $dummy->retrieveByDate(now())); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 689bdf1..9e9ee87 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,9 +3,8 @@ namespace JustBetter\MagentoPrices\Tests; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; +use Illuminate\Support\Facades\Http; use JustBetter\MagentoClient\Client\Magento; -use JustBetter\MagentoPrices\Retriever\DummyPriceRetriever; -use JustBetter\MagentoPrices\Retriever\DummySkuRetriever; use JustBetter\MagentoPrices\ServiceProvider; use Orchestra\Testbench\TestCase as BaseTestCase; use Spatie\Activitylog\ActivitylogServiceProvider; @@ -16,16 +15,9 @@ abstract class TestCase extends BaseTestCase protected function defineEnvironment($app): void { - config()->set('magento.base_url', ''); - config()->set('magento.access_token', '::token::'); - config()->set('magento.timeout', 30); - config()->set('magento.connect_timeout', 30); - - config()->set('magento.currency', 'EUR'); - config()->set('magento.precision', 2); + Magento::fake(); - config()->set('magento-prices.retrievers.sku', DummySkuRetriever::class); - config()->set('magento-prices.retrievers.price', DummyPriceRetriever::class); + Http::preventStrayRequests(); config()->set('database.default', 'testbench'); config()->set('database.connections.testbench', [ @@ -35,8 +27,6 @@ protected function defineEnvironment($app): void ]); activity()->disableLogging(); - - Magento::fake(); } protected function getPackageProviders($app): array @@ -44,6 +34,8 @@ protected function getPackageProviders($app): array return [ ServiceProvider::class, \JustBetter\MagentoClient\ServiceProvider::class, + \JustBetter\MagentoProducts\ServiceProvider::class, + \JustBetter\MagentoAsync\ServiceProvider::class, ActivitylogServiceProvider::class, ]; }