diff --git a/README.md b/README.md index 3b06872..283ca7a 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ Currently, the following attribute types are supported by this package: - `pim_catalog_boolean` - `pim_catalog_price_collection` - `pim_catalog_simpleselect` +- `pim_catalog_multiselect` ## Commands diff --git a/src/Actions/Product/SaveProduct.php b/src/Actions/Product/SaveProduct.php index 19028f9..669f386 100644 --- a/src/Actions/Product/SaveProduct.php +++ b/src/Actions/Product/SaveProduct.php @@ -33,6 +33,7 @@ public function save(ProductData $productData): void $product->update = $product->isDirty(['checksum']); } + $product->resetFailures(); $product->save(); } diff --git a/src/Actions/Product/UpdateProduct.php b/src/Actions/Product/UpdateProduct.php index 3b64b41..edb16ea 100644 --- a/src/Actions/Product/UpdateProduct.php +++ b/src/Actions/Product/UpdateProduct.php @@ -20,6 +20,7 @@ public function update(Product $product): void $product->update = false; $product->fail_count = 0; $product->failed_at = null; + $product->resetFailures(); $product->save(); } diff --git a/src/Actions/ProductModel/SaveProductModel.php b/src/Actions/ProductModel/SaveProductModel.php index efda575..a1f8140 100644 --- a/src/Actions/ProductModel/SaveProductModel.php +++ b/src/Actions/ProductModel/SaveProductModel.php @@ -33,6 +33,7 @@ public function save(ProductModelData $productModelData): void $productModel->update = $productModel->isDirty(['checksum']); } + $productModel->resetFailures(); $productModel->save(); } diff --git a/src/Actions/ProductModel/UpdateProductModel.php b/src/Actions/ProductModel/UpdateProductModel.php index f37c6b4..95e83b6 100644 --- a/src/Actions/ProductModel/UpdateProductModel.php +++ b/src/Actions/ProductModel/UpdateProductModel.php @@ -22,8 +22,7 @@ public function update(ProductModel $productModel): void $productModel->modified_at = now(); $productModel->update = false; - $productModel->fail_count = 0; - $productModel->failed_at = null; + $productModel->resetFailures(); $productModel->save(); } diff --git a/src/Akeneo/Types/NumberType.php b/src/Akeneo/Types/NumberType.php index 6cb92a5..e5dc4f3 100644 --- a/src/Akeneo/Types/NumberType.php +++ b/src/Akeneo/Types/NumberType.php @@ -14,7 +14,7 @@ class NumberType extends BaseType public function format(AttributeData $attributeData, mixed $value): float|int { if (! is_numeric($value)) { - throw new InvalidValueException('The given value is not numeric'); + throw new InvalidValueException($attributeData->code(), $value); } return $attributeData['decimals_allowed'] diff --git a/src/Akeneo/Types/PriceCollectionType.php b/src/Akeneo/Types/PriceCollectionType.php index 9bd693a..956fe78 100644 --- a/src/Akeneo/Types/PriceCollectionType.php +++ b/src/Akeneo/Types/PriceCollectionType.php @@ -25,9 +25,7 @@ public function format(AttributeData $attributeData, mixed $value): array } if (! isset($amount) || ! isset($currency)) { - throw new InvalidValueException( - 'Value is not valid for the price collection type. Got value: '.var_export($value, true) - ); + throw new InvalidValueException($attributeData->code(), $value); } return [ diff --git a/src/Exceptions/InvalidValueException.php b/src/Exceptions/InvalidValueException.php index 3d96ede..7de8606 100644 --- a/src/Exceptions/InvalidValueException.php +++ b/src/Exceptions/InvalidValueException.php @@ -6,5 +6,8 @@ class InvalidValueException extends Exception { - // + public function __construct(string $attribute, mixed $value) + { + parent::__construct('The given value "'.var_export($value, true).'" for attribute "'.$attribute.'" is invalid.'); + } } diff --git a/src/Jobs/Product/RetrieveProductJob.php b/src/Jobs/Product/RetrieveProductJob.php index 2572062..9bf2e58 100644 --- a/src/Jobs/Product/RetrieveProductJob.php +++ b/src/Jobs/Product/RetrieveProductJob.php @@ -8,6 +8,9 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use JustBetter\AkeneoProducts\Contracts\Product\RetrievesProduct; +use JustBetter\AkeneoProducts\Models\Product; +use Spatie\Activitylog\ActivityLogger; +use Throwable; class RetrieveProductJob implements ShouldBeUnique, ShouldQueue { @@ -37,4 +40,19 @@ public function tags(): array $this->identifier, ]; } + + public function failed(Throwable $throwable): void + { + /** @var ?Product $model */ + $model = Product::query()->firstWhere('identifier', '=', $this->identifier); + + $model?->failed(); + + activity() + ->when($model, function (ActivityLogger $logger, Product $product): ActivityLogger { + return $logger->on($product); + }) + ->useLog('error') + ->log('Failed to retrieve the product: '.$throwable->getMessage()); + } } diff --git a/src/Jobs/Product/SaveProductJob.php b/src/Jobs/Product/SaveProductJob.php index 214478c..0327bc8 100644 --- a/src/Jobs/Product/SaveProductJob.php +++ b/src/Jobs/Product/SaveProductJob.php @@ -9,6 +9,9 @@ use Illuminate\Queue\InteractsWithQueue; use JustBetter\AkeneoProducts\Contracts\Product\SavesProduct; use JustBetter\AkeneoProducts\Data\ProductData; +use JustBetter\AkeneoProducts\Models\Product; +use Spatie\Activitylog\ActivityLogger; +use Throwable; class SaveProductJob implements ShouldBeUnique, ShouldQueue { @@ -38,4 +41,22 @@ public function tags(): array $this->productData->identifier(), ]; } + + public function failed(Throwable $throwable): void + { + /** @var ?Product $model */ + $model = Product::query()->firstWhere('identifier', '=', $this->productData->identifier()); + + $model?->failed(); + + activity() + ->when($model, function (ActivityLogger $logger, Product $product): ActivityLogger { + return $logger->on($product); + }) + ->useLog('error') + ->withProperties([ + 'data' => $this->productData->toArray(), + ]) + ->log('Failed to save the product data: '.$throwable->getMessage()); + } } diff --git a/src/Jobs/Product/UpdateProductJob.php b/src/Jobs/Product/UpdateProductJob.php index 9af2cea..99685be 100644 --- a/src/Jobs/Product/UpdateProductJob.php +++ b/src/Jobs/Product/UpdateProductJob.php @@ -50,9 +50,8 @@ public function failed(Throwable $throwable): void ->on($this->product) ->useLog('error') ->withProperties([ - 'message' => $throwable->getMessage(), 'code' => $throwable->getCode(), ]) - ->log('Failed to update product in Akeneo'); + ->log('Failed to update product in Akeneo: '.$throwable->getMessage()); } } diff --git a/src/Jobs/ProductModel/RetrieveProductModelJob.php b/src/Jobs/ProductModel/RetrieveProductModelJob.php index 7ff5d86..9edb829 100644 --- a/src/Jobs/ProductModel/RetrieveProductModelJob.php +++ b/src/Jobs/ProductModel/RetrieveProductModelJob.php @@ -8,6 +8,9 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use JustBetter\AkeneoProducts\Contracts\ProductModel\RetrievesProductModel; +use JustBetter\AkeneoProducts\Models\ProductModel; +use Spatie\Activitylog\ActivityLogger; +use Throwable; class RetrieveProductModelJob implements ShouldBeUnique, ShouldQueue { @@ -37,4 +40,19 @@ public function tags(): array $this->code, ]; } + + public function failed(Throwable $throwable): void + { + /** @var ?ProductModel $model */ + $model = ProductModel::query()->firstWhere('code', '=', $this->code); + + $model?->failed(); + + activity() + ->when($model, function (ActivityLogger $logger, ProductModel $productModel): ActivityLogger { + return $logger->on($productModel); + }) + ->useLog('error') + ->log('Failed to retrieve the productmodel: '.$throwable->getMessage()); + } } diff --git a/src/Jobs/ProductModel/SaveProductModelJob.php b/src/Jobs/ProductModel/SaveProductModelJob.php index c40aa16..43fd373 100644 --- a/src/Jobs/ProductModel/SaveProductModelJob.php +++ b/src/Jobs/ProductModel/SaveProductModelJob.php @@ -9,6 +9,9 @@ use Illuminate\Queue\InteractsWithQueue; use JustBetter\AkeneoProducts\Contracts\ProductModel\SavesProductModel; use JustBetter\AkeneoProducts\Data\ProductModelData; +use JustBetter\AkeneoProducts\Models\ProductModel; +use Spatie\Activitylog\ActivityLogger; +use Throwable; class SaveProductModelJob implements ShouldBeUnique, ShouldQueue { @@ -38,4 +41,19 @@ public function tags(): array $this->productModelData->code(), ]; } + + public function failed(Throwable $throwable): void + { + /** @var ?ProductModel $model */ + $model = ProductModel::query()->firstWhere('code', '=', $this->productModelData->code()); + + $model?->failed(); + + activity() + ->when($model, function (ActivityLogger $logger, ProductModel $productModel): ActivityLogger { + return $logger->on($productModel); + }) + ->useLog('error') + ->log('Failed to save the productmodel: '.$throwable->getMessage()); + } } diff --git a/src/Models/Product.php b/src/Models/Product.php index 337fca4..6d16b7b 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use JustBetter\AkeneoProducts\Retrievers\Product\BaseProductRetriever; +use Spatie\Activitylog\LogOptions; +use Spatie\Activitylog\Traits\LogsActivity; /** * @property int $id @@ -26,6 +28,7 @@ */ class Product extends Model { + use LogsActivity; use SoftDeletes; protected $table = 'akeneo_products'; @@ -69,4 +72,19 @@ public function failed(): void $this->save(); } + + public function resetFailures(): void + { + $this->synchronize = true; + $this->fail_count = 0; + $this->failed_at = null; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->logOnly(['data']); + } } diff --git a/src/Models/ProductModel.php b/src/Models/ProductModel.php index c213d60..551563c 100644 --- a/src/Models/ProductModel.php +++ b/src/Models/ProductModel.php @@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use JustBetter\AkeneoProducts\Retrievers\ProductModel\BaseProductModelRetriever; +use Spatie\Activitylog\LogOptions; +use Spatie\Activitylog\Traits\LogsActivity; /** * @property int $id @@ -26,6 +28,7 @@ */ class ProductModel extends Model { + use LogsActivity; use SoftDeletes; protected $table = 'akeneo_product_models'; @@ -69,4 +72,19 @@ public function failed(): void $this->save(); } + + public function resetFailures(): void + { + $this->synchronize = true; + $this->fail_count = 0; + $this->failed_at = null; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->logOnly(['data']); + } } diff --git a/tests/Jobs/Product/RetrieveProductJobTest.php b/tests/Jobs/Product/RetrieveProductJobTest.php index c194337..ec4f65a 100644 --- a/tests/Jobs/Product/RetrieveProductJobTest.php +++ b/tests/Jobs/Product/RetrieveProductJobTest.php @@ -2,8 +2,10 @@ namespace JustBetter\AkeneoProducts\Tests\Jobs\Product; +use Exception; use JustBetter\AkeneoProducts\Contracts\Product\RetrievesProduct; use JustBetter\AkeneoProducts\Jobs\Product\RetrieveProductJob; +use JustBetter\AkeneoProducts\Models\Product; use JustBetter\AkeneoProducts\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -32,4 +34,30 @@ public function it_has_correct_tags_and_unique_id(): void $this->assertEquals(['identifier'], $job->tags()); $this->assertEquals('identifier', $job->uniqueId()); } + + #[Test] + public function it_can_fail(): void + { + /** @var Product $product */ + $product = Product::query()->create([ + 'identifier' => 'identifier', + 'data' => [], + ]); + + $job = new RetrieveProductJob('identifier'); + $job->failed(new Exception); + + $product->refresh(); + + $this->assertNotNull($product->failed_at); + } + + #[Test] + public function it_can_fail_without_product_model(): void + { + $job = new RetrieveProductJob('identifier'); + $job->failed(new Exception); + + $this->assertTrue(true, 'No exception thrown'); + } } diff --git a/tests/Jobs/Product/SaveProductJobTest.php b/tests/Jobs/Product/SaveProductJobTest.php index f38bfbc..545b5f3 100644 --- a/tests/Jobs/Product/SaveProductJobTest.php +++ b/tests/Jobs/Product/SaveProductJobTest.php @@ -2,9 +2,11 @@ namespace JustBetter\AkeneoProducts\Tests\Jobs\Product; +use Exception; use JustBetter\AkeneoProducts\Contracts\Product\SavesProduct; use JustBetter\AkeneoProducts\Data\ProductData; use JustBetter\AkeneoProducts\Jobs\Product\SaveProductJob; +use JustBetter\AkeneoProducts\Models\Product; use JustBetter\AkeneoProducts\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -58,4 +60,56 @@ public function it_has_correct_tags_and_unique_id(): void $this->assertEquals(['identifier'], $job->tags()); $this->assertEquals('identifier', $job->uniqueId()); } + + #[Test] + public function it_can_fail(): void + { + /** @var Product $product */ + $product = Product::query()->create([ + 'identifier' => 'identifier', + 'data' => [], + ]); + + $productData = ProductData::of([ + 'identifier' => 'identifier', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $job = new SaveProductJob($productData); + $job->failed(new Exception); + + $product->refresh(); + + $this->assertNotNull($product->failed_at); + } + + #[Test] + public function it_can_fail_without_product_model(): void + { + $productData = ProductData::of([ + 'identifier' => 'identifier', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $job = new SaveProductJob($productData); + $job->failed(new Exception); + + $this->assertTrue(true, 'No exception thrown'); + } } diff --git a/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php b/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php index 9e58588..8f9cdab 100644 --- a/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php +++ b/tests/Jobs/ProductModel/RetrieveProductModelJobTest.php @@ -2,8 +2,10 @@ namespace JustBetter\AkeneoProducts\Tests\Jobs\ProductModel; +use Exception; use JustBetter\AkeneoProducts\Contracts\ProductModel\RetrievesProductModel; use JustBetter\AkeneoProducts\Jobs\ProductModel\RetrieveProductModelJob; +use JustBetter\AkeneoProducts\Models\ProductModel; use JustBetter\AkeneoProducts\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -32,4 +34,30 @@ public function it_has_correct_tags_and_unique_id(): void $this->assertEquals(['code'], $job->tags()); $this->assertEquals('code', $job->uniqueId()); } + + #[Test] + public function it_can_fail(): void + { + /** @var ProductModel $productModel */ + $productModel = ProductModel::query()->create([ + 'code' => 'code', + 'data' => [], + ]); + + $job = new RetrieveProductModelJob('code'); + $job->failed(new Exception); + + $productModel->refresh(); + + $this->assertNotNull($productModel->failed_at); + } + + #[Test] + public function it_can_fail_without_product_model(): void + { + $job = new RetrieveProductModelJob('code'); + $job->failed(new Exception); + + $this->assertTrue(true, 'No exception thrown'); + } } diff --git a/tests/Jobs/ProductModel/SaveProductModelJobTest.php b/tests/Jobs/ProductModel/SaveProductModelJobTest.php index a462629..840c2cd 100644 --- a/tests/Jobs/ProductModel/SaveProductModelJobTest.php +++ b/tests/Jobs/ProductModel/SaveProductModelJobTest.php @@ -2,9 +2,11 @@ namespace JustBetter\AkeneoProducts\Tests\Jobs\ProductModel; +use Exception; use JustBetter\AkeneoProducts\Contracts\ProductModel\SavesProductModel; use JustBetter\AkeneoProducts\Data\ProductModelData; use JustBetter\AkeneoProducts\Jobs\ProductModel\SaveProductModelJob; +use JustBetter\AkeneoProducts\Models\ProductModel; use JustBetter\AkeneoProducts\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -58,4 +60,56 @@ public function it_has_correct_tags_and_unique_id(): void $this->assertEquals(['code'], $job->tags()); $this->assertEquals('code', $job->uniqueId()); } + + #[Test] + public function it_can_fail(): void + { + /** @var ProductModel $product */ + $product = ProductModel::query()->create([ + 'code' => 'code', + 'data' => [], + ]); + + $productModelData = ProductModelData::of([ + 'code' => 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $job = new SaveProductModelJob($productModelData); + $job->failed(new Exception); + + $product->refresh(); + + $this->assertNotNull($product->failed_at); + } + + #[Test] + public function it_can_fail_without_product_model(): void + { + $productModelData = ProductModelData::of([ + 'code' => 'code', + 'values' => [ + 'name' => [ + [ + 'locale' => 'nl_NL', + 'scope' => 'ecommerce', + 'data' => 'Ziggy', + ], + ], + ], + ]); + + $job = new SaveProductModelJob($productModelData); + $job->failed(new Exception); + + $this->assertTrue(true, 'No exception thrown'); + } }