From 2864cbf0664374661155f8a1f79d13ca066733b6 Mon Sep 17 00:00:00 2001 From: Vincent Boon Date: Tue, 21 Nov 2023 14:25:37 +0100 Subject: [PATCH 1/3] Check for deleted products in Magento --- composer.json | 16 +- ...02103_magento_products_retrieved_field.php | 20 +++ phpunit.xml | 27 ++-- src/Actions/CheckRemovedProducts.php | 30 ++++ src/Actions/DiscoverMagentoProducts.php | 46 ++++++ src/Actions/ProcessMagentoSkus.php | 1 + .../DiscoverMagentoProductsCommand.php | 12 +- src/Contracts/ChecksRemovedProducts.php | 8 + src/Contracts/DiscoversMagentoProducts.php | 10 ++ src/Events/ProductCreatedInMagentoEvent.php | 2 +- src/Events/ProductDeletedInMagentoEvent.php | 14 ++ src/Events/ProductDiscoveredEvent.php | 2 +- src/Jobs/CheckRemovedProductsJob.php | 31 ++++ src/Jobs/DiscoverMagentoProductsJob.php | 23 +-- src/Models/MagentoProduct.php | 20 +-- src/ServiceProvider.php | 4 + tests/Actions/CheckRemovedProductsTest.php | 41 +++++ tests/Actions/DiscoverMagentoProductsTest.php | 142 ++++++++++++++++++ .../DiscoverMagentoProductsCommandTest.php | 5 +- tests/Jobs/CheckRemovedProductsJobTest.php | 20 +++ tests/Jobs/DiscoverMagentoProductsJobTest.php | 49 ++---- 21 files changed, 430 insertions(+), 93 deletions(-) create mode 100644 database/migrations/2023_11_21_102103_magento_products_retrieved_field.php create mode 100644 src/Actions/CheckRemovedProducts.php create mode 100644 src/Actions/DiscoverMagentoProducts.php create mode 100644 src/Contracts/ChecksRemovedProducts.php create mode 100644 src/Contracts/DiscoversMagentoProducts.php create mode 100644 src/Events/ProductDeletedInMagentoEvent.php create mode 100644 src/Jobs/CheckRemovedProductsJob.php create mode 100644 tests/Actions/CheckRemovedProductsTest.php create mode 100644 tests/Actions/DiscoverMagentoProductsTest.php create mode 100644 tests/Jobs/CheckRemovedProductsJobTest.php diff --git a/composer.json b/composer.json index 95bbc78..a81ad5b 100644 --- a/composer.json +++ b/composer.json @@ -9,10 +9,13 @@ "laravel/framework": "^10.0" }, "require-dev": { - "laravel/pint": "^1.6", - "nunomaduro/larastan": "^2.5", - "phpstan/phpstan-mockery": "^1.1", - "phpunit/phpunit": "^10.0" + "laravel/pint": "^1.6", + "nunomaduro/larastan": "^2.5", + "orchestra/testbench": "^8.0", + "pestphp/pest": "^2.0", + "phpstan/phpstan-mockery": "^1.1", + "phpunit/phpunit": "^10.0", + "doctrine/dbal": "^3.7.1" }, "authors": [ { @@ -43,7 +46,10 @@ "fix-style": "pint" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "extra": { "laravel": { diff --git a/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php b/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php new file mode 100644 index 0000000..904d158 --- /dev/null +++ b/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php @@ -0,0 +1,20 @@ +boolean('retrieved')->default(false)->after('last_checked'); + }); + } + + public function down(): void + { + Schema::dropColumns('magento_products', ['retrieved']); + } +}; diff --git a/phpunit.xml b/phpunit.xml index bcf40ad..c2f1f77 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,14 @@ - - - - ./tests/* - - - - - ./src - - + + + + ./tests/* + + + + + + ./src + + diff --git a/src/Actions/CheckRemovedProducts.php b/src/Actions/CheckRemovedProducts.php new file mode 100644 index 0000000..2eefd19 --- /dev/null +++ b/src/Actions/CheckRemovedProducts.php @@ -0,0 +1,30 @@ +where('exists_in_magento', '=', true) + ->where('retrieved', '=', false); + + $skus = $query->select(['sku'])->get(); + + $query->update([ + 'exists_in_magento' => false, + ]); + + $skus->each(fn (MagentoProduct $product) => ProductDeletedInMagentoEvent::dispatch($product->sku)); + } + + public static function bind(): void + { + app()->singleton(ChecksRemovedProducts::class, static::class); + } +} diff --git a/src/Actions/DiscoverMagentoProducts.php b/src/Actions/DiscoverMagentoProducts.php new file mode 100644 index 0000000..88769d6 --- /dev/null +++ b/src/Actions/DiscoverMagentoProducts.php @@ -0,0 +1,46 @@ +update(['retrieved' => false]); + } + + $skus = $this->magentoSkus->retrieve($page); + + $hasNextPage = $skus->count() == config('magento-products.page_size'); + + if ($hasNextPage) { + $batch->add(new DiscoverMagentoProductsJob($page + 1)); + } + + MagentoProduct::query() + ->whereIn('sku', $skus) + ->update(['retrieved' => true]); + + $this->processor->process($skus); + } + + public static function bind(): void + { + app()->singleton(DiscoversMagentoProducts::class, static::class); + } +} diff --git a/src/Actions/ProcessMagentoSkus.php b/src/Actions/ProcessMagentoSkus.php index 2b45d7a..5aa6c5b 100644 --- a/src/Actions/ProcessMagentoSkus.php +++ b/src/Actions/ProcessMagentoSkus.php @@ -15,6 +15,7 @@ public function process(Enumerable $skus): void ->whereIn('sku', $skus) ->where('exists_in_magento', true) ->select(['sku']) + ->distinct() ->get() ->pluck('sku'); diff --git a/src/Commands/DiscoverMagentoProductsCommand.php b/src/Commands/DiscoverMagentoProductsCommand.php index 64b5390..b52a200 100644 --- a/src/Commands/DiscoverMagentoProductsCommand.php +++ b/src/Commands/DiscoverMagentoProductsCommand.php @@ -3,6 +3,8 @@ namespace JustBetter\MagentoProducts\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Bus; +use JustBetter\MagentoProducts\Jobs\CheckRemovedProductsJob; use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob; class DiscoverMagentoProductsCommand extends Command @@ -13,11 +15,11 @@ class DiscoverMagentoProductsCommand extends Command public function handle(): int { - $this->info('Dispatching...'); - - DiscoverMagentoProductsJob::dispatch(); - - $this->info('Done!'); + Bus::batch([new DiscoverMagentoProductsJob]) + ->name('Discover Magento Products') + ->then(fn () => CheckRemovedProductsJob::dispatch()) + ->onQueue(config('magento-products.queue')) + ->dispatch(); return static::SUCCESS; } diff --git a/src/Contracts/ChecksRemovedProducts.php b/src/Contracts/ChecksRemovedProducts.php new file mode 100644 index 0000000..59be795 --- /dev/null +++ b/src/Contracts/ChecksRemovedProducts.php @@ -0,0 +1,8 @@ +onQueue(config('magento-products.queue')); + } + + public function handle(ChecksRemovedProducts $contract): void + { + $contract->check(); + } +} diff --git a/src/Jobs/DiscoverMagentoProductsJob.php b/src/Jobs/DiscoverMagentoProductsJob.php index c336c17..555be1d 100644 --- a/src/Jobs/DiscoverMagentoProductsJob.php +++ b/src/Jobs/DiscoverMagentoProductsJob.php @@ -9,8 +9,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use JustBetter\MagentoProducts\Contracts\ProcessesMagentoSkus; -use JustBetter\MagentoProducts\Contracts\RetrievesMagentoSkus; +use JustBetter\MagentoProducts\Contracts\DiscoversMagentoProducts; class DiscoverMagentoProductsJob implements ShouldBeUnique, ShouldQueue { @@ -31,23 +30,13 @@ public function __construct(public int $page = 0) $this->onQueue(config('magento-products.queue')); } - public function handle( - RetrievesMagentoSkus $retrievesMagentoSkus, - ProcessesMagentoSkus $processesMagentoSkus - ): void { - $skus = $retrievesMagentoSkus->retrieve($this->page); - - $hasNextPage = $skus->count() == config('magento-products.page_size', 50); - - if ($hasNextPage) { - if ($this->batching()) { - $this->batch()->add(new static($this->page + 1)); /** @phpstan-ignore-line */ - } else { - static::dispatch($this->page + 1); - } + public function handle(DiscoversMagentoProducts $contract): void + { + if ($this->batch() === null || $this->batch()->cancelled()) { + return; } - $processesMagentoSkus->process($skus); + $contract->discover($this->page, $this->batch()); } public function tags(): array diff --git a/src/Models/MagentoProduct.php b/src/Models/MagentoProduct.php index abf8893..eb7bee8 100644 --- a/src/Models/MagentoProduct.php +++ b/src/Models/MagentoProduct.php @@ -2,10 +2,9 @@ namespace JustBetter\MagentoProducts\Models; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use JustBetter\MagentoProducts\Contracts\ChecksMagentoExistence; +use Illuminate\Support\Carbon; /** * @property int $id @@ -15,6 +14,9 @@ * @property bool $enabled * @property ?array $data * @property ?Carbon $last_checked + * @property bool $retrieved + * @property ?Carbon $created_at + * @property ?Carbon $updated_at */ class MagentoProduct extends Model { @@ -24,6 +26,7 @@ class MagentoProduct extends Model 'data' => 'array', 'exists_in_magento' => 'boolean', 'last_checked' => 'datetime', + 'retrieved' => 'boolean', ]; public static function findBySku(string $sku, string $store = null): ?static @@ -36,17 +39,4 @@ public static function findBySku(string $sku, string $store = null): ?static return $item; } - - /** - * @deprecated Use the action ChecksMagentoExistence instead - * - * @codeCoverageIgnore - */ - public static function existsInMagento(string $sku): bool - { - /** @var ChecksMagentoExistence $action */ - $action = app(ChecksMagentoExistence::class); - - return $action->exists($sku); - } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 367818d..99a9205 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -8,6 +8,8 @@ use JustBetter\MagentoProducts\Actions\CheckKnownProducts; use JustBetter\MagentoProducts\Actions\CheckMagentoEnabled; use JustBetter\MagentoProducts\Actions\CheckMagentoExistence; +use JustBetter\MagentoProducts\Actions\CheckRemovedProducts; +use JustBetter\MagentoProducts\Actions\DiscoverMagentoProducts; use JustBetter\MagentoProducts\Actions\ProcessMagentoSkus; use JustBetter\MagentoProducts\Actions\RetrieveMagentoSkus; use JustBetter\MagentoProducts\Actions\RetrieveProductData; @@ -30,6 +32,8 @@ public function register(): void ProcessMagentoSkus::bind(); RetrieveMagentoSkus::bind(); RetrieveProductData::bind(); + DiscoverMagentoProducts::bind(); + CheckRemovedProducts::bind(); } public function boot(): void diff --git a/tests/Actions/CheckRemovedProductsTest.php b/tests/Actions/CheckRemovedProductsTest.php new file mode 100644 index 0000000..5256efb --- /dev/null +++ b/tests/Actions/CheckRemovedProductsTest.php @@ -0,0 +1,41 @@ +create([ + 'sku' => '::sku_1::', + 'exists_in_magento' => true, + 'retrieved' => false, + ]); + + MagentoProduct::query()->create([ + 'sku' => '::sku_2::', + 'exists_in_magento' => false, + 'retrieved' => false, + ]); + + /** @var CheckRemovedProducts $action */ + $action = app(CheckRemovedProducts::class); + $action->check(); + + /** @var ?MagentoProduct $removedProduct */ + $removedProduct = MagentoProduct::query()->firstWhere('sku', '=', '::sku_1::'); + + $this->assertNotNull($removedProduct); + $this->assertFalse($removedProduct->exists_in_magento); + + Event::assertDispatchedTimes(ProductDeletedInMagentoEvent::class, 1); + } +} diff --git a/tests/Actions/DiscoverMagentoProductsTest.php b/tests/Actions/DiscoverMagentoProductsTest.php new file mode 100644 index 0000000..a9c77f6 --- /dev/null +++ b/tests/Actions/DiscoverMagentoProductsTest.php @@ -0,0 +1,142 @@ +set('magento-products.page_size', 2); + } + + public function test_it_processes_single_page(): void + { + $skus = collect(['123']); + + $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) use ($skus) { + $mock->shouldReceive('retrieve')->with(0)->once() + ->andReturn($skus); + }); + + $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) use ($skus) { + $mock->shouldReceive('process')->with($skus)->once(); + }); + + $job = new DiscoverMagentoProductsJob(); + $job->withFakeBatch(); + + /** @var Batch $batch */ + $batch = $job->batch(); + + /** @var DiscoverMagentoProducts $action */ + $action = app(DiscoverMagentoProducts::class); + $action->discover(0, $batch); + } + + public function test_it_dispatches_next_job(): void + { + $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('retrieve')->with(0)->once() + ->andReturn(collect(['123', '456'])); + }); + + $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('process')->once(); + }); + + $job = new DiscoverMagentoProductsJob(); + $job->withFakeBatch(); + + /** @var Batch $batch */ + $batch = $job->batch(); + + /** @var DiscoverMagentoProducts $action */ + $action = app(DiscoverMagentoProducts::class); + $action->discover(0, $batch); + + /** @var ?DiscoverMagentoProductsJob $addedJob */ + $addedJob = $job->batch()->added[0] ?? null; + + $this->assertNotNull($addedJob); + $this->assertEquals(1, $addedJob->page); + } + + public function test_it_sets_retrieved_false(): void + { + MagentoProduct::query()->create([ + 'sku' => '123', + 'exists_in_magento' => true, + 'retrieved' => true, + ]); + + $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('retrieve')->with(0)->once() + ->andReturn(collect()); + }); + + $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('process')->once(); + }); + + $job = new DiscoverMagentoProductsJob(); + $job->withFakeBatch(); + + /** @var Batch $batch */ + $batch = $job->batch(); + + /** @var DiscoverMagentoProducts $action */ + $action = app(DiscoverMagentoProducts::class); + $action->discover(0, $batch); + + /** @var ?MagentoProduct $product */ + $product = MagentoProduct::query()->first(); + + $this->assertNotNull($product); + $this->assertFalse($product->retrieved); + } + + public function test_it_sets_retrieved_true(): void + { + MagentoProduct::query()->create([ + 'sku' => '123', + 'exists_in_magento' => true, + 'retrieved' => true, + ]); + + $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('retrieve')->with(0)->once() + ->andReturn(collect(['123'])); + }); + + $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { + $mock->shouldReceive('process')->once(); + }); + + $job = new DiscoverMagentoProductsJob(); + $job->withFakeBatch(); + + /** @var Batch $batch */ + $batch = $job->batch(); + + /** @var DiscoverMagentoProducts $action */ + $action = app(DiscoverMagentoProducts::class); + $action->discover(0, $batch); + + /** @var ?MagentoProduct $product */ + $product = MagentoProduct::query()->first(); + + $this->assertNotNull($product); + $this->assertTrue($product->retrieved); + } +} diff --git a/tests/Commands/DiscoverMagentoProductsCommandTest.php b/tests/Commands/DiscoverMagentoProductsCommandTest.php index 98a1311..5aa2ec2 100644 --- a/tests/Commands/DiscoverMagentoProductsCommandTest.php +++ b/tests/Commands/DiscoverMagentoProductsCommandTest.php @@ -3,6 +3,7 @@ namespace JustBetter\MagentoProducts\Tests\Commands; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Testing\Fakes\PendingBatchFake; use JustBetter\MagentoProducts\Commands\DiscoverMagentoProductsCommand; use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob; use JustBetter\MagentoProducts\Tests\TestCase; @@ -15,6 +16,8 @@ public function test_it_dispatches_job(): void $this->artisan(DiscoverMagentoProductsCommand::class); - Bus::assertDispatched(DiscoverMagentoProductsJob::class); + Bus::assertBatched(function (PendingBatchFake $batch) { + return $batch->jobs->count() === 1 && get_class($batch->jobs->first()) === DiscoverMagentoProductsJob::class; + }); } } diff --git a/tests/Jobs/CheckRemovedProductsJobTest.php b/tests/Jobs/CheckRemovedProductsJobTest.php new file mode 100644 index 0000000..1e9d503 --- /dev/null +++ b/tests/Jobs/CheckRemovedProductsJobTest.php @@ -0,0 +1,20 @@ +mock(ChecksRemovedProducts::class, function (MockInterface $mock): void { + $mock->shouldReceive('check')->once(); + }); + + CheckRemovedProductsJob::dispatch(); + } +} diff --git a/tests/Jobs/DiscoverMagentoProductsJobTest.php b/tests/Jobs/DiscoverMagentoProductsJobTest.php index 3e60aa4..6c780ca 100644 --- a/tests/Jobs/DiscoverMagentoProductsJobTest.php +++ b/tests/Jobs/DiscoverMagentoProductsJobTest.php @@ -2,57 +2,40 @@ namespace JustBetter\MagentoProducts\Tests\Jobs; -use JustBetter\MagentoProducts\Contracts\ProcessesMagentoSkus; -use JustBetter\MagentoProducts\Contracts\RetrievesMagentoSkus; +use Illuminate\Support\Facades\Bus; +use JustBetter\MagentoProducts\Contracts\DiscoversMagentoProducts; use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob; use JustBetter\MagentoProducts\Tests\TestCase; use Mockery\MockInterface; class DiscoverMagentoProductsJobTest extends TestCase { - protected function setUp(): void + public function test_it_calls_action(): void { - parent::setUp(); - } - - public function test_it_processes_single_page(): void - { - config()->set('magento-products.page_size', 2); - - $skus = collect(['123']); - - $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) use ($skus) { - $mock->shouldReceive('retrieve')->with(0)->once() - ->andReturn($skus); + $this->mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void { + $mock->shouldReceive('discover')->once(); }); - $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) use ($skus) { - $mock->shouldReceive('process')->with($skus)->once(); - }); + $job = new DiscoverMagentoProductsJob(0); + $job->withFakeBatch(); - DiscoverMagentoProductsJob::dispatch(0); + Bus::dispatch($job); } - public function test_it_processes_two_pages(): void + public function test_it_stops_when_batch_is_cancelled(): void { - config()->set('magento-products.page_size', 2); - - $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) { - $mock->shouldReceive('retrieve')->with(0)->once() - ->andReturn(collect(['123', '456'])); - - $mock->shouldReceive('retrieve')->with(1)->once() - ->andReturn(collect(['789'])); + $this->mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('discover'); }); - $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { - $mock->shouldReceive('process')->twice(); - }); + $job = new DiscoverMagentoProductsJob(0); + $job->withFakeBatch(); + $job->batch()?->cancel(); - DiscoverMagentoProductsJob::dispatch(0); + Bus::dispatch($job); } - public function test_tags(): void + public function test_it_has_tags(): void { $job = new DiscoverMagentoProductsJob(0); From ad846981c8d09c30b0d6ecd9e6783511b955c821 Mon Sep 17 00:00:00 2001 From: Vincent Boon Date: Tue, 21 Nov 2023 14:28:35 +0100 Subject: [PATCH 2/3] Adjust workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bbeb413..9e0e0d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: os: [ubuntu-latest] php: [8.1, 8.2] laravel: [10.*] - stability: [prefer-lowest, prefer-stable] + stability: [prefer-stable] include: - laravel: 10.* testbench: 8.* From 95e513cf514a8911b42ada74e6009b80688735c8 Mon Sep 17 00:00:00 2001 From: Vincent Boon Date: Wed, 22 Nov 2023 09:57:25 +0100 Subject: [PATCH 3/3] Update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1daa00c..142b7ef 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@

This package tracks if products exist in Magento by storing the status locally in the DB. -We developed this to prevent multiple calls when multiple packages need to check product existance in Magento. -This package does do the assumption that once a product exists in Magento it will always be there. +We developed this to prevent multiple calls when multiple packages need to check product existence in Magento. ## Installation @@ -26,6 +25,9 @@ $schedule->command(\JustBetter\MagentoProducts\Commands\CheckKnownProductsExiste $schedule->command(\JustBetter\MagentoProducts\Commands\DiscoverMagentoProductsCommand::class)->daily(); ``` +> [!IMPORTANT] +> This package requires Job Batching + ## Usage ### Checking if a product exists in Magento