diff --git a/config/Migrations/20250810031402_CreateProductPhotos.php b/config/Migrations/20250810031402_CreateProductPhotos.php index 77fe785..cfcaa22 100644 --- a/config/Migrations/20250810031402_CreateProductPhotos.php +++ b/config/Migrations/20250810031402_CreateProductPhotos.php @@ -79,7 +79,7 @@ class CreateProductPhotos extends BaseMigration 'product_sku_id', ], [ 'name' => 'PRODUCT_PHOTOS_BY_PRODUCT_SKU_ID', - 'unique' => true, + 'unique' => false, ]); $table->create(); diff --git a/config/Migrations/20251101150008_AddPrimarySkuPhotoToProductPhotos.php b/config/Migrations/20251101150008_AddPrimarySkuPhotoToProductPhotos.php new file mode 100644 index 0000000..bf88b2b --- /dev/null +++ b/config/Migrations/20251101150008_AddPrimarySkuPhotoToProductPhotos.php @@ -0,0 +1,25 @@ +table('product_photos'); + $table->addColumn('primary_sku_photo', 'boolean', [ + 'default' => false, + 'limit' => 11, + 'null' => false, + ]); + $table->update(); + } +} diff --git a/src/Controller/ProductCategoryAttributesController.php b/src/Controller/ProductCategoryAttributesController.php index 868e890..aa820aa 100644 --- a/src/Controller/ProductCategoryAttributesController.php +++ b/src/Controller/ProductCategoryAttributesController.php @@ -11,7 +11,6 @@ use Cake\ORM\Table; use Cake\ORM\TableRegistry; use CakeProducts\Controller\AppController; use CheeseCake\Controller\Traits\OverrideTableTrait; -; use CakeProducts\Model\Enum\ProductCategoryAttributeTypeId; use CakeProducts\Model\Table\ProductCategoryAttributesTable; diff --git a/src/Controller/ProductPhotosController.php b/src/Controller/ProductPhotosController.php index d2f64c1..c4e25fb 100644 --- a/src/Controller/ProductPhotosController.php +++ b/src/Controller/ProductPhotosController.php @@ -74,8 +74,8 @@ class ProductPhotosController extends AppController $product = $productPhotosTable->Products->get($this->request->getData('product_id')); $path = $product->id; if ($this->request->getData('product_sku_id')) { - $productSku = $productPhotosTable->ProductSkus->get($this->request->getData('product_sku_id')); - $path .= DS . 'skus' . DS . $productSku->id; + $productSku = $productPhotosTable->ProductSkus->find()->where(['ProductSkus.id' => $this->request->getData('product_sku_id'), 'ProductSkus.product_id' => $product->id])->first(); + $path = $productSku ? $path . DS . 'skus' . DS . $productSku->id : $path; } /** * @var UploadedFileInterface $photoObject @@ -98,14 +98,13 @@ class ProductPhotosController extends AppController $postData['product_category_id'] = $product->product_category_id ?? null; $postData['photo_dir'] = $path; $postData['photo_filename'] = $uuid; + $productPhoto = $productPhotosTable->patchEntity($productPhoto, $postData); if ($productPhotosTable->save($productPhoto)) { $this->Flash->success(__('The product photo has been saved.')); return $this->redirect(['action' => 'index']); } - dd($productPhoto->product_category_id); -// dd(print_r($productPhoto->getErrors(), true)); $this->Flash->error(__('The product photo could not be saved. Please, try again.')); } $productCategory = $productPhoto->product_category_id ? $productPhotosTable->ProductCategories->find()->where(['internal_id' => $productPhoto->product_category_id ?? '-1'])->first() : null; diff --git a/src/Model/Behavior/ThirdToggleBehavior.php b/src/Model/Behavior/ThirdToggleBehavior.php new file mode 100644 index 0000000..59a1364 --- /dev/null +++ b/src/Model/Behavior/ThirdToggleBehavior.php @@ -0,0 +1,57 @@ + + */ + protected array $_defaultConfig = [ + 'field' => 'primary', + 'on' => 'afterSave', // afterSave (without transactions) or beforeSave (with transactions) + 'scopeFields' => [], + 'scope' => [], + 'findOrder' => null, // null = autodetect modified/created, false to disable + 'implementedMethods' => [], // to prevent conflict with public toggleField method + ]; + + /** + * @param \Cake\Datasource\EntityInterface $entity + * + * @return array + */ + protected function buildConditions(EntityInterface $entity) { + $conditions = $this->getConfig('scope'); + $scopeFields = (array)$this->getConfig('scopeFields'); + + foreach ($scopeFields as $scopeField) { + if ($entity->get($scopeField) === null) { + continue; + } + $conditions[$scopeField] = $entity->get($scopeField); + } +// dd($conditions); + + return $conditions; + } +} diff --git a/src/Model/Entity/ProductPhoto.php b/src/Model/Entity/ProductPhoto.php index 46177e4..a925d3a 100644 --- a/src/Model/Entity/ProductPhoto.php +++ b/src/Model/Entity/ProductPhoto.php @@ -17,6 +17,7 @@ use Cake\ORM\Entity; * @property string $photo_filename * @property bool $primary_photo * @property bool $primary_category_photo + * @property bool $primary_sku_photo * @property int $photo_position * @property bool $enabled * @property DateTime $created @@ -46,6 +47,7 @@ class ProductPhoto extends Entity 'photo_filename' => true, 'primary_photo' => true, 'primary_category_photo' => true, + 'primary_sku_photo' => true, 'photo_position' => true, 'enabled' => true, 'created' => true, diff --git a/src/Model/Table/ProductPhotosTable.php b/src/Model/Table/ProductPhotosTable.php index 33c8609..3653cf0 100644 --- a/src/Model/Table/ProductPhotosTable.php +++ b/src/Model/Table/ProductPhotosTable.php @@ -58,6 +58,7 @@ class ProductPhotosTable extends Table ); $this->addBehavior('Timestamp'); + $this->addBehavior('Tools.Toggle', [ 'field' => 'primary_photo', 'scopeFields' => ['product_id'], @@ -74,6 +75,14 @@ class ProductPhotosTable extends Table 'product_category_id IS NOT' => null, ], ]); + $this->addBehavior('CakeProducts.ThirdToggle', [ + 'field' => 'primary_sku_photo', + 'scopeFields' => ['product_sku_id'], + 'scope' => [ + 'deleted IS' => null, + 'product_sku_id IS NOT' => null, + ], + ]); $this->belongsTo('Products', [ 'foreignKey' => 'product_id', 'joinType' => 'LEFT', diff --git a/tests/Fixture/ProductPhotosFixture.php b/tests/Fixture/ProductPhotosFixture.php index ccda639..ee177b3 100644 --- a/tests/Fixture/ProductPhotosFixture.php +++ b/tests/Fixture/ProductPhotosFixture.php @@ -27,6 +27,7 @@ class ProductPhotosFixture extends TestFixture 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f58.png', 'primary_photo' => 1, 'primary_category_photo' => 0, + 'primary_sku_photo' => 0, 'photo_position' => 100, 'enabled' => 1, 'created' => '2025-08-10 04:32:10', @@ -43,6 +44,7 @@ class ProductPhotosFixture extends TestFixture 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f51.png', 'primary_photo' => 0, 'primary_category_photo' => 1, + 'primary_sku_photo' => 0, 'photo_position' => 100, 'enabled' => 1, 'created' => '2025-08-10 04:32:10', @@ -59,6 +61,7 @@ class ProductPhotosFixture extends TestFixture 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f58.png', 'primary_photo' => 0, 'primary_category_photo' => 0, + 'primary_sku_photo' => 0, 'photo_position' => 100, 'enabled' => 1, 'created' => '2025-08-10 04:32:10', @@ -74,6 +77,40 @@ class ProductPhotosFixture extends TestFixture 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f51.png', 'primary_photo' => 0, 'primary_category_photo' => 0, + 'primary_sku_photo' => 0, + 'photo_position' => 100, + 'enabled' => 1, + 'created' => '2025-08-10 04:32:10', + 'modified' => '2025-08-10 04:32:10', + 'deleted' => null, + ], + + [ + 'id' => '2c386086-f4c5-4093-bea5-ee9c29479f11', + 'product_id' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'product_sku_id' => '3a477e3e-7977-4813-81f6-f85949613979', + 'product_category_id' => '6d223283-361b-4f9f-a7f1-c97aa0ca4c23', + 'photo_dir' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f58.png', + 'primary_photo' => 0, + 'primary_category_photo' => 0, + 'primary_sku_photo' => 1, + 'photo_position' => 100, + 'enabled' => 1, + 'created' => '2025-08-10 04:32:10', + 'modified' => '2025-08-10 04:32:10', + 'deleted' => null, + ], + [ + 'id' => '2c386086-f4c5-4093-bea5-ee9c29479f12', + 'product_id' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'product_sku_id' => '3a477e3e-7977-4813-81f6-f85949613979', + 'product_category_id' => '6d223283-361b-4f9f-a7f1-c97aa0ca4c23', + 'photo_dir' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'photo_filename' => '2c386086-f4c5-4093-bea5-ee9c29479f51.png', + 'primary_photo' => 0, + 'primary_category_photo' => 0, + 'primary_sku_photo' => 0, 'photo_position' => 100, 'enabled' => 1, 'created' => '2025-08-10 04:32:10', diff --git a/tests/TestCase/Controller/ProductPhotosControllerTest.php b/tests/TestCase/Controller/ProductPhotosControllerTest.php index fce9ff8..682a9ad 100644 --- a/tests/TestCase/Controller/ProductPhotosControllerTest.php +++ b/tests/TestCase/Controller/ProductPhotosControllerTest.php @@ -148,7 +148,7 @@ class ProductPhotosControllerTest extends BaseControllerTest * * @uses \CakeProducts\Controller\ProductPhotosController::add */ - public function testAddPostLoggedInSuccessDefaultProductPhoto(): void + public function testAddPostLoggedInSuccessDefaultPhoto(): void { $cntBefore = $this->ProductPhotos->find()->count(); @@ -186,13 +186,14 @@ class ProductPhotosControllerTest extends BaseControllerTest ]); $data = [ 'product_id' => $productId, - 'product_sku_id' => '', 'primary_photo' => 1, - 'primary_category_photo' => 1, + 'primary_category_photo' => 0, + 'primary_sku_photo' => 0, 'photo' => $image, 'enabled' => 1, ]; $this->post($url, $data); +// dd($this->_response); $this->assertResponseCode(302); $this->assertRedirectContains('product-photos'); @@ -202,13 +203,16 @@ class ProductPhotosControllerTest extends BaseControllerTest $new = $this->ProductPhotos->find()->where(['product_id' => $productId])->orderBy(['created' => 'DESC'])->first(); $this->assertTrue($new->primary_photo); - $this->assertTrue($new->primary_category_photo); + $this->assertFalse($new->primary_category_photo); + $this->assertFalse($new->primary_sku_photo); + $this->assertEquals($productId, $new->product_id); $primaryPhotosCountAfter = $this->ProductPhotos->find()->where(['product_id' => $productId, 'primary_photo' => true])->count(); $primaryPhotoAfter = $this->ProductPhotos->find()->where(['product_id' => $productId, 'primary_photo' => true])->first(); $this->assertEquals(1, $primaryPhotosCountAfter); $this->assertNotEquals($primaryPhotoBefore->id, $primaryPhotoAfter->id); + $this->assertEquals($new->id, $primaryPhotoAfter->id); } /** @@ -266,6 +270,7 @@ class ProductPhotosControllerTest extends BaseControllerTest 'product_sku_id' => '', 'primary_photo' => 0, 'primary_category_photo' => 1, + 'primary_sku_photo' => 0, 'photo' => $image, 'enabled' => 1, ]; @@ -290,6 +295,89 @@ class ProductPhotosControllerTest extends BaseControllerTest } + /** + * Test add method + * + * Tests a POST request to the add action with a logged in user + * + * @return void + * @throws Exception + * + * @TODO handle toggle behavior when scope field is nullable + * + * @uses \CakeProducts\Controller\ProductPhotosController::add + */ + public function testAddPostLoggedInSuccessDefaultSkuPhoto(): void + { + $cntBefore = $this->ProductPhotos->find()->count(); + + // $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductPhotos', + 'action' => 'add', + ]; + $file = Configure::readOrFail('App.paths.testWebroot') . 'images' . DS . 'cake_icon.png'; + $toUseFile = Configure::readOrFail('App.paths.testWebroot') . 'images' . DS . 'cake_icon_copy.png'; + if (!file_exists($file)) { + $this->fail('Test image did not exist'); + } + if (!copy($file, $toUseFile)) { + $this->fail('Failed to copy test image'); + } + $productId = 'cfc98a9a-29b2-44c8-b587-8156adc05317'; + $skuId = '3a477e3e-7977-4813-81f6-f85949613979'; + + $primarySkuPhotosCountBefore = $this->ProductPhotos->find()->where(['product_sku_id' => $skuId, 'primary_sku_photo' => true])->count(); + $primarySkuPhotoBefore = $this->ProductPhotos->find()->where(['product_sku_id' => $skuId, 'primary_sku_photo' => true])->first(); + + $this->assertEquals(1, $primarySkuPhotosCountBefore); + + + $image = new UploadedFile( + $toUseFile, // stream or path to file representing the temp file + 12345, // the filesize in bytes + UPLOAD_ERR_OK, // the upload/error status + 'cake_icon.png', // the filename as sent by the client + 'image/png' // the mimetype as sent by the client + ); + $this->configRequest([ + 'files' => [ + 'photo' => $image, + ], + ]); + $data = [ + 'product_id' => $productId, + 'product_sku_id' => $skuId, + 'primary_photo' => 0, + 'primary_category_photo' => 0, + 'primary_sku_photo' => 1, + 'photo' => $image, + 'enabled' => 1, + ]; + $this->post($url, $data); + + $this->assertResponseCode(302); + $this->assertRedirectContains('product-photos'); + + $cntAfter = $this->ProductPhotos->find()->count(); + $this->assertEquals($cntBefore + 1, $cntAfter); + + $new = $this->ProductPhotos->find()->where(['product_sku_id' => $skuId])->orderBy(['created' => 'DESC'])->first(); + + $this->assertFalse($new->primary_photo); + $this->assertFalse($new->primary_category_photo); + $this->assertTrue($new->primary_sku_photo); + + $primarySkuPhotosCountAfter = $this->ProductPhotos->find()->where(['product_sku_id' => $skuId, 'primary_sku_photo' => true])->count(); + $primarySkuPhotoAfter = $this->ProductPhotos->find()->where(['product_sku_id' => $skuId, 'primary_sku_photo' => true])->first(); + + $this->assertEquals(1, $primarySkuPhotosCountAfter); + $this->assertNotEquals($primarySkuPhotoBefore->id, $primarySkuPhotoAfter->id); + $this->assertEquals($new->id, $primarySkuPhotoAfter->id); + + } + /** * Test add method *