diff --git a/README.md b/README.md index d1fa146..856f504 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ This package is built to easily create products in Akeneo from your ERP or other Aside from attributes, you can also set a family, categories and all other data you would normally be able to via the [API](https://api.akeneo.com/api-reference.html#Products). -> **Note:** This package does not have support for product models yet. - For more advanced use cases, it is also possible to add your own data to the payload. Products are retrieved and updated in small chunks to spread the load if your project has access to a lot of products. Updates of these products are only sent to Akeneo if something has been modified to prevent unnecessary requests. @@ -39,19 +37,20 @@ Publish the configuration of the package. php artisan vendor:publish --provider="JustBetter\AkeneoProducts\ServiceProvider" --tag=config ``` -Add the following command to your scheduler. +Add the following commands to your scheduler. ```php command(ProcessProductsCommand::class)->everyMinute(); +$schedule->command(\JustBetter\AkeneoProducts\Commands\Product\ProcessProductsCommand::class)->everyMinute(); +$schedule->command(\JustBetter\AkeneoProducts\Commands\ProductModel\ProcessProductModelsCommand::class)->everyMinute(); ``` ## How it works -When a product is retrieved, it will fetch the product from the (external) source. If the retrieved data is not null, it will be saved in the database. If the payload of the model has been changed, the `update`-property of the model will be set to `true`. This way, the package is aware of updates and can do them in small batches. The numbers can be tweaked in your configuration file. +This section will describe the process of products but product models will be similar in usage. + +When a product is retrieved, it will fetch the product from the (external) source. If the retrieved data is not null, it will be saved in the database. If the payload of the database model has been changed, the `update`-property of the model will be set to `true`. This way, the package is aware of updates and can do them in small batches. The numbers can be tweaked in your retriever class. When a product has already been retrieved in the past, the `retrieve`-property of the model can be set to `true` in order to automatically fetch the data again. This is done by the `ProcessProductsCommand`, if added to your scheduler. @@ -65,9 +64,12 @@ This package does **not** contain a way to retrieve "all" products. If you wish ```php 'default', - /* Max tries before a product sync will automatically be turned off. */ - 'tries' => 3, + /* Configuration for the retrievers. */ + 'retrievers' => [ - /* Amount of products to be retrieved at once when processing. */ - 'retrieve_batch_size' => 100, - - /* Amount of products to be updated at once when processing. */ - 'update_batch_size' => 25, - - /* Class responsible to retrieve the products from. */ - 'retriever' => \JustBetter\AkeneoProducts\Retrievers\ProductRetriever::class, + /* Class responsible to retrieve the products from. */ + 'product' => \JustBetter\AkeneoProducts\Retrievers\Product\ProductRetriever::class, + /* Class responsible to retrieve the product models from. */ + 'product_model' => \JustBetter\AkeneoProducts\Retrievers\ProductModel\ProductModelRetriever::class, + ], ]; diff --git a/database/migrations/2023_08_22_083000_create_akeneo_product_models_table.php b/database/migrations/2023_08_22_083000_create_akeneo_product_models_table.php new file mode 100644 index 0000000..f3ae8b4 --- /dev/null +++ b/database/migrations/2023_08_22_083000_create_akeneo_product_models_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('code'); + $table->boolean('synchronize')->default(true); + $table->boolean('retrieve')->default(false); + $table->boolean('update')->default(false); + $table->integer('fail_count')->default(0); + $table->json('data'); + $table->string('checksum')->nullable(); + $table->timestamp('retrieved_at')->nullable(); + $table->timestamp('modified_at')->nullable(); + $table->timestamp('failed_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('akeneo_product_models'); + } +}; diff --git a/src/Actions/ProcessProducts.php b/src/Actions/Product/ProcessProducts.php similarity index 69% rename from src/Actions/ProcessProducts.php rename to src/Actions/Product/ProcessProducts.php index 6ead6d4..b89e9a9 100644 --- a/src/Actions/ProcessProducts.php +++ b/src/Actions/Product/ProcessProducts.php @@ -1,11 +1,12 @@ where('update', '=', true) ->update(['update' => false]); - /** @var int $retrieveBatchSize */ - $retrieveBatchSize = config('akeneo-products.retrieve_batch_size'); + $retrieveBatchSize = BaseProductRetriever::current()->retrieveBatchSize(); // Dispatch jobs for all products that should be retrieved. Product::query() @@ -28,8 +28,7 @@ public function process(): void ->pluck('identifier') ->each(fn (string $identifier) => RetrieveProductJob::dispatch($identifier)); - /** @var int $updateBatchSize */ - $updateBatchSize = config('akeneo-products.update_batch_size'); + $updateBatchSize = BaseProductRetriever::current()->updateBatchSize(); // Dispatch jobs for all products that are should be updated. Product::query() diff --git a/src/Actions/RetrieveProduct.php b/src/Actions/Product/RetrieveProduct.php similarity index 51% rename from src/Actions/RetrieveProduct.php rename to src/Actions/Product/RetrieveProduct.php index d5ebc56..f7bee90 100644 --- a/src/Actions/RetrieveProduct.php +++ b/src/Actions/Product/RetrieveProduct.php @@ -1,16 +1,16 @@ retrieve($identifier); + $product = BaseProductRetriever::current()->retrieve($identifier); if ($product !== null) { SaveProductJob::dispatch($product); diff --git a/src/Actions/SaveProduct.php b/src/Actions/Product/SaveProduct.php similarity index 89% rename from src/Actions/SaveProduct.php rename to src/Actions/Product/SaveProduct.php index bd4f586..19028f9 100644 --- a/src/Actions/SaveProduct.php +++ b/src/Actions/Product/SaveProduct.php @@ -1,8 +1,8 @@ where('retrieve', '=', true) + ->where('update', '=', true) + ->update(['update' => false]); + + $retrieveBatchSize = BaseProductModelRetriever::current()->retrieveBatchSize(); + + // Dispatch jobs for all product models that should be retrieved. + ProductModel::query() + ->scopes('shouldRetrieve') + ->limit($retrieveBatchSize) + ->get() + ->pluck('code') + ->each(fn (string $code) => RetrieveProductModelJob::dispatch($code)); + + $updateBatchSize = BaseProductModelRetriever::current()->updateBatchSize(); + + // Dispatch jobs for all product models that are should be updated. + ProductModel::query() + ->scopes('shouldUpdate') + ->limit($updateBatchSize) + ->get() + ->each(fn (ProductModel $productModel) => UpdateProductModelJob::dispatch($productModel)); + } + + public static function bind(): void + { + app()->singleton(ProcessesProductModels::class, static::class); + } +} diff --git a/src/Actions/ProductModel/RetrieveProductModel.php b/src/Actions/ProductModel/RetrieveProductModel.php new file mode 100644 index 0000000..0ed052e --- /dev/null +++ b/src/Actions/ProductModel/RetrieveProductModel.php @@ -0,0 +1,24 @@ +retrieve($code); + + if ($productModel !== null) { + SaveProductModelJob::dispatch($productModel); + } + } + + public static function bind(): void + { + app()->singleton(RetrievesProductModel::class, static::class); + } +} diff --git a/src/Actions/ProductModel/SaveProductModel.php b/src/Actions/ProductModel/SaveProductModel.php new file mode 100644 index 0000000..efda575 --- /dev/null +++ b/src/Actions/ProductModel/SaveProductModel.php @@ -0,0 +1,43 @@ +firstOrCreate( + [ + 'code' => $productModelData->code(), + ], + [ + 'data' => [], + ], + ); + + $data = $productModelData->toArray(); + + $encoded = json_encode($data); + + $productModel->data = $data; + $productModel->checksum = md5($encoded ?: ''); + $productModel->retrieved_at = now(); + $productModel->retrieve = false; + + if (! $productModel->update) { + $productModel->update = $productModel->isDirty(['checksum']); + } + + $productModel->save(); + } + + public static function bind(): void + { + app()->singleton(SavesProductModel::class, static::class); + } +} diff --git a/src/Actions/ProductModel/UpdateProductModel.php b/src/Actions/ProductModel/UpdateProductModel.php new file mode 100644 index 0000000..7f87f6e --- /dev/null +++ b/src/Actions/ProductModel/UpdateProductModel.php @@ -0,0 +1,33 @@ +data; + + unset($data['code']); + + $this->akeneo->getProductModelApi()->upsert($productModel->code, $data); + + $productModel->modified_at = now(); + $productModel->update = false; + $productModel->save(); + } + + public static function bind(): void + { + app()->singleton(UpdatesProductModel::class, static::class); + } +} diff --git a/src/Commands/ProcessProductsCommand.php b/src/Commands/Product/ProcessProductsCommand.php similarity index 75% rename from src/Commands/ProcessProductsCommand.php rename to src/Commands/Product/ProcessProductsCommand.php index f03a6bf..bcd3580 100644 --- a/src/Commands/ProcessProductsCommand.php +++ b/src/Commands/Product/ProcessProductsCommand.php @@ -1,9 +1,9 @@ argument('code'); + + RetrieveProductModelJob::dispatch($code); + + return static::SUCCESS; + } +} diff --git a/src/Commands/ProductModel/UpdateProductModelCommand.php b/src/Commands/ProductModel/UpdateProductModelCommand.php new file mode 100644 index 0000000..22aa9df --- /dev/null +++ b/src/Commands/ProductModel/UpdateProductModelCommand.php @@ -0,0 +1,29 @@ +argument('code'); + + /** @var ProductModel $productModel */ + $productModel = ProductModel::query() + ->where('code', '=', $code) + ->firstOrFail(); + + UpdateProductModelJob::dispatch($productModel); + + return static::SUCCESS; + } +} diff --git a/src/Contracts/ProcessesProducts.php b/src/Contracts/Product/ProcessesProducts.php similarity index 58% rename from src/Contracts/ProcessesProducts.php rename to src/Contracts/Product/ProcessesProducts.php index d5253ce..e029b07 100644 --- a/src/Contracts/ProcessesProducts.php +++ b/src/Contracts/Product/ProcessesProducts.php @@ -1,6 +1,6 @@ 'required|string', + 'values' => 'nullable|array', + 'family' => 'nullable|string', + 'categories' => 'nullable|array', + ]; + + public function code(): string + { + return $this['code']; + } + + public function family(): ?string + { + return $this['family']; + } + + public function categories(): ?array + { + return $this['categories']; + } + + public function values(): array + { + return $this['values']; + } +} diff --git a/src/Jobs/ProcessProductsJob.php b/src/Jobs/Product/ProcessProductsJob.php similarity index 83% rename from src/Jobs/ProcessProductsJob.php rename to src/Jobs/Product/ProcessProductsJob.php index 7ab906b..a1bcef9 100644 --- a/src/Jobs/ProcessProductsJob.php +++ b/src/Jobs/Product/ProcessProductsJob.php @@ -1,13 +1,13 @@ onQueue(config('akeneo-products.queue')); + } + + public function handle(ProcessesProductModels $processesProductModels): void + { + $processesProductModels->process(); + } +} diff --git a/src/Jobs/ProductModel/RetrieveProductModelJob.php b/src/Jobs/ProductModel/RetrieveProductModelJob.php new file mode 100644 index 0000000..7ff5d86 --- /dev/null +++ b/src/Jobs/ProductModel/RetrieveProductModelJob.php @@ -0,0 +1,40 @@ +onQueue(config('akeneo-products.queue')); + } + + public function handle(RetrievesProductModel $retrievesProductModel): void + { + $retrievesProductModel->retrieve($this->code); + } + + public function uniqueId(): string + { + return $this->code; + } + + public function tags(): array + { + return [ + $this->code, + ]; + } +} diff --git a/src/Jobs/ProductModel/SaveProductModelJob.php b/src/Jobs/ProductModel/SaveProductModelJob.php new file mode 100644 index 0000000..c40aa16 --- /dev/null +++ b/src/Jobs/ProductModel/SaveProductModelJob.php @@ -0,0 +1,41 @@ +onQueue(config('akeneo-products.queue')); + } + + public function handle(SavesProductModel $savesProductModel): void + { + $savesProductModel->save($this->productModelData); + } + + public function uniqueId(): string + { + return $this->productModelData->code(); + } + + public function tags(): array + { + return [ + $this->productModelData->code(), + ]; + } +} diff --git a/src/Jobs/ProductModel/UpdateProductModelJob.php b/src/Jobs/ProductModel/UpdateProductModelJob.php new file mode 100644 index 0000000..3176e5d --- /dev/null +++ b/src/Jobs/ProductModel/UpdateProductModelJob.php @@ -0,0 +1,60 @@ +onQueue(config('akeneo-products.queue')); + } + + public function handle(UpdatesProductModel $updatesProductModel): void + { + $updatesProductModel->update($this->productModel); + } + + public function uniqueId(): string + { + return $this->productModel->code; + } + + public function tags(): array + { + return [ + $this->productModel->code, + ]; + } + + public function failed(Throwable $throwable): void + { + $this->productModel->failed(); + + activity() + ->on($this->productModel) + ->withProperties([ + 'message' => $throwable->getMessage(), + 'code' => $throwable->getCode(), + 'metadata' => [ + 'level' => 'error', + ], + ]) + ->log('Failed to update product model in Akeneo'); + } +} diff --git a/src/Models/Product.php b/src/Models/Product.php index 9efb763..337fca4 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; +use JustBetter\AkeneoProducts\Retrievers\Product\BaseProductRetriever; /** * @property int $id @@ -60,8 +61,7 @@ public function failed(): void $this->fail_count++; $this->failed_at = now(); - /** @var int $tries */ - $tries = config('akeneo-products.tries', 0); + $tries = BaseProductRetriever::current()->tries(); if ($tries > 0 && $this->fail_count >= $tries) { $this->synchronize = false; diff --git a/src/Models/ProductModel.php b/src/Models/ProductModel.php new file mode 100644 index 0000000..c213d60 --- /dev/null +++ b/src/Models/ProductModel.php @@ -0,0 +1,72 @@ + 'boolean', + 'retrieve' => 'boolean', + 'update' => 'boolean', + 'retrieved_at' => 'datetime', + 'modified_at' => 'datetime', + 'failed_at' => 'datetime', + 'data' => 'array', + ]; + + public function scopeShouldRetrieve(Builder $builder): Builder + { + return $builder + ->where('synchronize', '=', true) + ->where('retrieve', '=', true); + } + + public function scopeShouldUpdate(Builder $builder): Builder + { + return $builder + ->where('synchronize', '=', true) + ->where('update', '=', true); + } + + public function failed(): void + { + $this->fail_count++; + $this->failed_at = now(); + + $tries = BaseProductModelRetriever::current()->tries(); + + if ($tries > 0 && $this->fail_count >= $tries) { + $this->synchronize = false; + } + + $this->save(); + } +} diff --git a/src/Retrievers/BaseRetriever.php b/src/Retrievers/BaseRetriever.php index 42e331b..3c2d46f 100644 --- a/src/Retrievers/BaseRetriever.php +++ b/src/Retrievers/BaseRetriever.php @@ -2,20 +2,29 @@ namespace JustBetter\AkeneoProducts\Retrievers; -use JustBetter\AkeneoProducts\Data\ProductData; - abstract class BaseRetriever { - abstract public function retrieve(string $identifier): ?ProductData; + /* Max tries before a resource sync will automatically be turned off. */ + protected int $tries = 3; + + /* Amount of resources to be retrieved at once when processing. */ + protected int $retrieveBatchSize = 100; - public static function current(): BaseRetriever + /* Amount of resources to be updated at once when processing. */ + protected int $updateBatchSize = 25; + + public function tries(): int { - /** @var string $class */ - $class = config('akeneo-products.retriever'); + return $this->tries; + } - /** @var BaseRetriever $retriever */ - $retriever = app($class); + public function retrieveBatchSize(): int + { + return $this->retrieveBatchSize; + } - return $retriever; + public function updateBatchSize(): int + { + return $this->updateBatchSize; } } diff --git a/src/Retrievers/Product/BaseProductRetriever.php b/src/Retrievers/Product/BaseProductRetriever.php new file mode 100644 index 0000000..1c8205b --- /dev/null +++ b/src/Retrievers/Product/BaseProductRetriever.php @@ -0,0 +1,22 @@ +app->runningInConsole()) { $this->commands([ + // Product ProcessProductsCommand::class, RetrieveProductCommand::class, UpdateProductCommand::class, + + // Product models + ProcessProductModelsCommand::class, + RetrieveProductModelCommand::class, + UpdateProductModelCommand::class, ]); } diff --git a/tests/Actions/ProcessProductsTest.php b/tests/Actions/Product/ProcessProductsTest.php similarity index 82% rename from tests/Actions/ProcessProductsTest.php rename to tests/Actions/Product/ProcessProductsTest.php index 94ea950..b6eca21 100644 --- a/tests/Actions/ProcessProductsTest.php +++ b/tests/Actions/Product/ProcessProductsTest.php @@ -1,12 +1,13 @@ set('akeneo-products.retrieve_batch_size', 2); - config()->set('akeneo-products.update_batch_size', 2); + config()->set('akeneo-products.retrievers.product', ProductRetriever::class); Product::query()->insert([ [ diff --git a/tests/Actions/RetrieveProductTest.php b/tests/Actions/Product/RetrieveProductTest.php similarity index 55% rename from tests/Actions/RetrieveProductTest.php rename to tests/Actions/Product/RetrieveProductTest.php index 7b795d6..05d1a92 100644 --- a/tests/Actions/RetrieveProductTest.php +++ b/tests/Actions/Product/RetrieveProductTest.php @@ -1,11 +1,11 @@ set('akeneo-products.retriever', TestRetriever::class); + config()->set('akeneo-products.retrievers.product', ProductRetriever::class); /** @var RetrieveProduct $action */ $action = app(RetrieveProduct::class); diff --git a/tests/Actions/SaveProductTest.php b/tests/Actions/Product/SaveProductTest.php similarity index 95% rename from tests/Actions/SaveProductTest.php rename to tests/Actions/Product/SaveProductTest.php index bffe3f7..d5947ae 100644 --- a/tests/Actions/SaveProductTest.php +++ b/tests/Actions/Product/SaveProductTest.php @@ -1,8 +1,8 @@ set('akeneo-products.retrievers.product_model', ProductModelRetriever::class); + + ProductModel::query()->insert([ + [ + 'code' => 'code-1', + 'synchronize' => false, + 'retrieve' => true, + 'update' => false, + 'data' => '{}', + ], + [ + 'code' => 'code-2', + 'synchronize' => true, + 'retrieve' => true, + 'update' => false, + 'data' => '{}', + ], + [ + 'code' => 'code-3', + 'synchronize' => true, + 'retrieve' => false, + 'update' => false, + 'data' => '{}', + ], + [ + 'code' => 'code-4', + 'synchronize' => true, + 'retrieve' => false, + 'update' => true, + 'data' => '{}', + ], + [ + 'code' => 'code-5', + 'synchronize' => true, + 'retrieve' => false, + 'update' => true, + 'data' => '{}', + ], + [ + 'code' => 'code-6', + 'synchronize' => true, + 'retrieve' => false, + 'update' => true, + 'data' => '{}', + ], + ]); + + /** @var ProcessProductModels $action */ + $action = app(ProcessProductModels::class); + $action->process(); + + Bus::assertDispatched(RetrieveProductModelJob::class, 1); + Bus::assertDispatched(UpdateProductModelJob::class, 2); + } +} diff --git a/tests/Actions/ProductModel/RetrieveProductModelTest.php b/tests/Actions/ProductModel/RetrieveProductModelTest.php new file mode 100644 index 0000000..9a96981 --- /dev/null +++ b/tests/Actions/ProductModel/RetrieveProductModelTest.php @@ -0,0 +1,26 @@ +set('akeneo-products.retrievers.product_model', ProductModelRetriever::class); + + /** @var RetrieveProductModel $action */ + $action = app(RetrieveProductModel::class); + $action->retrieve('code'); + + Bus::assertDispatched(SaveProductModelJob::class); + } +} diff --git a/tests/Actions/ProductModel/SaveProductModelTest.php b/tests/Actions/ProductModel/SaveProductModelTest.php new file mode 100644 index 0000000..faa0125 --- /dev/null +++ b/tests/Actions/ProductModel/SaveProductModelTest.php @@ -0,0 +1,104 @@ + 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + 'color' => [ + [ + 'locale' => null, + 'scope' => null, + 'data' => 'purple', + ], + ], + ], + ]); + + /** @var SaveProductModel $action */ + $action = app(SaveProductModel::class); + $action->save($productModelData); + + /** @var ProductModel $productModel */ + $productModel = ProductModel::query() + ->where('code', '=', 'code') + ->firstOrFail(); + + $this->assertTrue($productModel->update); + + $productModel->update = false; + $productModel->save(); + + $productModelData = ProductModelData::of([ + 'code' => 'code', + 'values' => [ + 'color' => [ + [ + 'locale' => null, + 'scope' => null, + 'data' => 'purple', + ], + ], + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $action->save($productModelData); + + $productModel->refresh(); + + $this->assertTrue($productModel->update); + + $productModel->update = false; + $productModel->save(); + + $productModelData = ProductModelData::of([ + 'code' => 'code', + 'values' => [ + 'color' => [ + [ + 'locale' => null, + 'scope' => null, + 'data' => 'purple', + ], + ], + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $action->save($productModelData); + + $productModel->refresh(); + + $this->assertFalse($productModel->update); + } +} diff --git a/tests/Actions/ProductModel/UpdateProductModelTest.php b/tests/Actions/ProductModel/UpdateProductModelTest.php new file mode 100644 index 0000000..885c52f --- /dev/null +++ b/tests/Actions/ProductModel/UpdateProductModelTest.php @@ -0,0 +1,58 @@ + 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]; + + /** @var ProductModel $productModel */ + $productModel = ProductModel::query()->create([ + 'code' => 'code', + 'data' => $payload, + ]); + + /** @var UpdateProductModel $action */ + $action = app(UpdateProductModel::class); + $action->update($productModel); + + Http::assertSent(function (Request $request) use ($payload): bool { + if ($request->url() === 'akeneo/api/oauth/v1/token') { + return true; + } + + if ($request->url() === 'akeneo/api/rest/v1/product-models/code') { + return $request->data() === $payload; + } + + return false; + }); + + $this->assertNotNull($productModel->modified_at); + $this->assertFalse($productModel->update); + } +} diff --git a/tests/Commands/ProcessProductsCommandTest.php b/tests/Commands/Product/ProcessProductsCommandTest.php similarity index 73% rename from tests/Commands/ProcessProductsCommandTest.php rename to tests/Commands/Product/ProcessProductsCommandTest.php index 20cc0be..dfad7ce 100644 --- a/tests/Commands/ProcessProductsCommandTest.php +++ b/tests/Commands/Product/ProcessProductsCommandTest.php @@ -1,11 +1,11 @@ artisan(ProcessProductModelsCommand::class); + + $artisan + ->assertSuccessful() + ->run(); + + Bus::assertDispatched(ProcessProductModelsJob::class); + } +} diff --git a/tests/Commands/ProductModel/RetrieveProductModelCommandTest.php b/tests/Commands/ProductModel/RetrieveProductModelCommandTest.php new file mode 100644 index 0000000..c0e9ce3 --- /dev/null +++ b/tests/Commands/ProductModel/RetrieveProductModelCommandTest.php @@ -0,0 +1,29 @@ +artisan(RetrieveProductModelCommand::class, [ + 'code' => 'code', + ]); + + $artisan + ->assertSuccessful() + ->run(); + + Bus::assertDispatched(RetrieveProductModelJob::class); + } +} diff --git a/tests/Commands/ProductModel/UpdateProductModelCommandTest.php b/tests/Commands/ProductModel/UpdateProductModelCommandTest.php new file mode 100644 index 0000000..f23300b --- /dev/null +++ b/tests/Commands/ProductModel/UpdateProductModelCommandTest.php @@ -0,0 +1,36 @@ +create([ + 'code' => 'code', + 'data' => [], + ]); + + /** @var PendingCommand $artisan */ + $artisan = $this->artisan(UpdateProductModelCommand::class, [ + 'code' => $productModel->code, + ]); + + $artisan + ->assertSuccessful() + ->run(); + + Bus::assertDispatched(UpdateProductModelJob::class); + } +} diff --git a/tests/Data/ProductModelDataTest.php b/tests/Data/ProductModelDataTest.php new file mode 100644 index 0000000..d1694b9 --- /dev/null +++ b/tests/Data/ProductModelDataTest.php @@ -0,0 +1,43 @@ + 'code', + 'family' => 'family', + 'categories' => [ + 'category', + ], + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $this->assertEquals('code', $productModelData->code()); + $this->assertEquals('family', $productModelData->family()); + $this->assertEquals(['category'], $productModelData->categories()); + $this->assertEquals([ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], $productModelData->values()); + } +} diff --git a/tests/Fakes/Retrievers/TestRetriever.php b/tests/Fakes/Retrievers/Product/ProductRetriever.php similarity index 61% rename from tests/Fakes/Retrievers/TestRetriever.php rename to tests/Fakes/Retrievers/Product/ProductRetriever.php index 378f0c8..062093c 100644 --- a/tests/Fakes/Retrievers/TestRetriever.php +++ b/tests/Fakes/Retrievers/Product/ProductRetriever.php @@ -1,12 +1,18 @@ 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + } +} diff --git a/tests/Jobs/ProcessProductsJobTest.php b/tests/Jobs/Product/ProcessProductsJobTest.php similarity index 71% rename from tests/Jobs/ProcessProductsJobTest.php rename to tests/Jobs/Product/ProcessProductsJobTest.php index d9f3a69..e9326c2 100644 --- a/tests/Jobs/ProcessProductsJobTest.php +++ b/tests/Jobs/Product/ProcessProductsJobTest.php @@ -1,9 +1,9 @@ mock(ProcessesProductModels::class, function (MockInterface $mock): void { + $mock + ->shouldReceive('process') + ->once() + ->andReturn(); + }); + + ProcessProductModelsJob::dispatch(); + } +} diff --git a/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php b/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php new file mode 100644 index 0000000..e44d877 --- /dev/null +++ b/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php @@ -0,0 +1,34 @@ +mock(RetrievesProductModel::class, function (MockInterface $mock): void { + $mock + ->shouldReceive('retrieve') + ->with('code') + ->once() + ->andReturn(); + }); + + RetrieveProductModelJob::dispatch('code'); + } + + /** @test */ + public function it_has_correct_tags_and_unique_id(): void + { + $job = new RetrieveProductModelJob('code'); + + $this->assertEquals(['code'], $job->tags()); + $this->assertEquals('code', $job->uniqueId()); + } +} diff --git a/tests/Jobs/ProductModel/SaveProductModelJobTest.php b/tests/Jobs/ProductModel/SaveProductModelJobTest.php new file mode 100644 index 0000000..20ae5a3 --- /dev/null +++ b/tests/Jobs/ProductModel/SaveProductModelJobTest.php @@ -0,0 +1,60 @@ + 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $this->mock(SavesProductModel::class, function (MockInterface $mock): void { + $mock + ->shouldReceive('save') + ->once() + ->andReturn(); + }); + + SaveProductModelJob::dispatch($productModelData); + } + + /** @test */ + public function it_has_correct_tags_and_unique_id(): void + { + $productModelData = ProductModelData::of([ + 'code' => 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $job = new SaveProductModelJob($productModelData); + + $this->assertEquals(['code'], $job->tags()); + $this->assertEquals('code', $job->uniqueId()); + } +} diff --git a/tests/Jobs/ProductModel/UpdateProductModelJobTest.php b/tests/Jobs/ProductModel/UpdateProductModelJobTest.php new file mode 100644 index 0000000..a88ec5b --- /dev/null +++ b/tests/Jobs/ProductModel/UpdateProductModelJobTest.php @@ -0,0 +1,62 @@ +create([ + 'code' => 'code', + 'data' => [], + ]); + + $this->mock(UpdatesProductModel::class, function (MockInterface $mock): void { + $mock + ->shouldReceive('update') + ->once() + ->andReturn(); + }); + + UpdateProductModelJob::dispatch($productModel); + } + + /** @test */ + public function it_can_fail(): void + { + /** @var ProductModel $productModel */ + $productModel = ProductModel::query()->create([ + 'code' => 'code', + 'data' => [], + ]); + + $job = new UpdateProductModelJob($productModel); + $job->failed(new Exception); + + $this->assertNotNull($productModel->failed_at); + } + + /** @test */ + public function it_has_correct_tags_and_unique_id(): void + { + /** @var ProductModel $productModel */ + $productModel = ProductModel::query()->create([ + 'code' => 'code', + 'data' => [], + ]); + + $job = new UpdateProductModelJob($productModel); + + $this->assertEquals(['code'], $job->tags()); + $this->assertEquals('code', $job->uniqueId()); + } +} diff --git a/tests/Models/ProductModelTest.php b/tests/Models/ProductModelTest.php new file mode 100644 index 0000000..a3eb826 --- /dev/null +++ b/tests/Models/ProductModelTest.php @@ -0,0 +1,35 @@ +set('akeneo-products.retrievers.product_model', ProductModelRetriever::class); + + /** @var ProductModel $productModel */ + $productModel = ProductModel::query()->create([ + 'code' => 'code', + 'synchronize' => true, + 'data' => [], + ]); + + $productModel->failed(); + + $this->assertNotNull($productModel->failed_at); + $this->assertEquals(1, $productModel->fail_count); + $this->assertTrue($productModel->synchronize); + + $productModel->failed(); + + $this->assertNotNull($productModel->failed_at); + $this->assertEquals(2, $productModel->fail_count); + $this->assertFalse($productModel->synchronize); + } +} diff --git a/tests/Models/ProductTest.php b/tests/Models/ProductTest.php index b5b241d..147bef7 100644 --- a/tests/Models/ProductTest.php +++ b/tests/Models/ProductTest.php @@ -3,6 +3,7 @@ namespace JustBetter\AkeneoProducts\Tests\Models; use JustBetter\AkeneoProducts\Models\Product; +use JustBetter\AkeneoProducts\Tests\Fakes\Retrievers\Product\ProductRetriever; use JustBetter\AkeneoProducts\Tests\TestCase; class ProductTest extends TestCase @@ -10,7 +11,7 @@ class ProductTest extends TestCase /** @test */ public function it_can_stop_the_synchronization(): void { - config()->set('akeneo-products.tries', 2); + config()->set('akeneo-products.retrievers.product', ProductRetriever::class); /** @var Product $product */ $product = Product::query()->create([ diff --git a/tests/Retrievers/BaseRetrieverTest.php b/tests/Retrievers/BaseRetrieverTest.php deleted file mode 100644 index 4019c1e..0000000 --- a/tests/Retrievers/BaseRetrieverTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertNotNull($retriever instanceof ProductRetriever); - } -} diff --git a/tests/Retrievers/Product/BaseProductRetrieverTest.php b/tests/Retrievers/Product/BaseProductRetrieverTest.php new file mode 100644 index 0000000..a26f77b --- /dev/null +++ b/tests/Retrievers/Product/BaseProductRetrieverTest.php @@ -0,0 +1,18 @@ +assertTrue($retriever instanceof ProductRetriever); + } +} diff --git a/tests/Retrievers/ProductRetrieverTest.php b/tests/Retrievers/Product/ProductRetrieverTest.php similarity index 78% rename from tests/Retrievers/ProductRetrieverTest.php rename to tests/Retrievers/Product/ProductRetrieverTest.php index dfb2323..d16fef5 100644 --- a/tests/Retrievers/ProductRetrieverTest.php +++ b/tests/Retrievers/Product/ProductRetrieverTest.php @@ -1,9 +1,9 @@ assertTrue($retriever instanceof ProductModelRetriever); + } +} diff --git a/tests/Retrievers/ProductModel/ProductModelRetrieverTest.php b/tests/Retrievers/ProductModel/ProductModelRetrieverTest.php new file mode 100644 index 0000000..02ab056 --- /dev/null +++ b/tests/Retrievers/ProductModel/ProductModelRetrieverTest.php @@ -0,0 +1,20 @@ +expectException(NotImplementedException::class); + + /** @var ProductModelRetriever $retriever */ + $retriever = app(ProductModelRetriever::class); + $retriever->retrieve('code'); + } +}