diff --git a/config/Migrations/20250415090249_CreateProductSkus.php b/config/Migrations/20250415090249_CreateProductSkus.php new file mode 100644 index 0000000..970397b --- /dev/null +++ b/config/Migrations/20250415090249_CreateProductSkus.php @@ -0,0 +1,62 @@ +table('product_skus', ['id' => false, 'primary_key' => ['id']]); + $table->addColumn('id', 'uuid', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('product_id', 'uuid', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('sku', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('barcode', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]); + $table->addColumn('price', 'decimal', [ + 'default' => null, + 'precision' => 15, + 'scale' => 6, + 'null' => true, + ]); + $table->addColumn('cost', 'decimal', [ + 'default' => null, + 'precision' => 15, + 'scale' => 6, + 'null' => true, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => true, + ]); + $table->addColumn('deleted', 'datetime', [ + 'default' => null, + 'null' => true, + ]); + $table->create(); + } +} diff --git a/src/Controller/ProductSkusController.php b/src/Controller/ProductSkusController.php new file mode 100644 index 0000000..3ec6b41 --- /dev/null +++ b/src/Controller/ProductSkusController.php @@ -0,0 +1,139 @@ +_defaultTable = 'CakeProducts.ProductSkus'; +// $this->_tableConfigKey = 'CakeProducts.ProductSkus.table'; + } + + /** + * Index method + * + * @return \Cake\Http\Response|null|void Renders view + */ + public function index() + { + $query = $this->ProductSkus->find() + ->contain(['Products']); + $productSkus = $this->paginate($query); + + $this->set(compact('productSkus')); + } + + /** + * View method + * + * @param string|null $id Product Skus id. + * @return \Cake\Http\Response|null|void Renders view + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function view($id = null) + { + $productSku = $this->ProductSkus->get($id, contain: ['Products']); + $this->set(compact('productSku')); + } + + /** + * Add method + * + * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise. + */ + public function add() + { + $productSku = $this->ProductSkus->newEmptyEntity(); + if ($this->request->is('post')) { + $postData = $this->request->getData(); + $saveOptions = [ + 'associated' => [], + ]; + // Log::debug(print_r('$postData', true)); +// Log::debug(print_r($postData, true)); +// Log::debug(print_r('$saveOptions', true)); +// Log::debug(print_r($saveOptions, true)); + $productSku = $this->ProductSkus->patchEntity($productSku, $postData, $saveOptions); + if ($this->ProductSkus->save($productSku)) { + $this->Flash->success(__('The product sku has been saved.')); + + return $this->redirect(['action' => 'index']); + } + Log::debug(print_r('$productSku->getErrors() next - failed in productSkus/add', true)); + Log::debug(print_r($productSku->getErrors(), true)); + $this->Flash->error(__('The product skus could not be saved. Please, try again.')); + } + $products = $this->ProductSkus->Products->find('list', limit: 200)->all(); + $this->set(compact('productSku', 'products')); + } + + /** + * Edit method + * + * @param string|null $id Product Skus id. + * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise. + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function edit($id = null) + { + $productSku = $this->ProductSkus->get($id, contain: []); + if ($this->request->is(['patch', 'post', 'put'])) { + $postData = $this->request->getData(); + $saveOptions = [ + 'associated' => [], + ]; + // Log::debug(print_r('$postData', true)); +// Log::debug(print_r($postData, true)); +// Log::debug(print_r('$saveOptions', true)); +// Log::debug(print_r($saveOptions, true)); + $productSku = $this->ProductSkus->patchEntity($productSku, $postData, $saveOptions); + if ($this->ProductSkus->save($productSku)) { + $this->Flash->success(__('The product skus has been saved.')); + + return $this->redirect(['action' => 'index']); + } + Log::debug(print_r('$productSku->getErrors() next - failed in productSkus/edit', true)); + Log::debug(print_r($productSku->getErrors(), true)); + $this->Flash->error(__('The product skus could not be saved. Please, try again.')); + } + $products = $this->ProductSkus->Products->find('list', limit: 200)->all(); + $this->set(compact('productSku', 'products')); + } + + /** + * Delete method + * + * @param string|null $id Product Skus id. + * @return \Cake\Http\Response|null Redirects to index. + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function delete($id = null) + { + $this->request->allowMethod(['post', 'delete']); + $productSku = $this->ProductSkus->get($id); + if ($this->ProductSkus->delete($productSku)) { + $this->Flash->success(__('The product skus has been deleted.')); + } else { + $this->Flash->error(__('The product skus could not be deleted. Please, try again.')); + } + + return $this->redirect(['action' => 'index']); + } +} diff --git a/src/Model/Entity/ProductSku.php b/src/Model/Entity/ProductSku.php new file mode 100644 index 0000000..f90cdb4 --- /dev/null +++ b/src/Model/Entity/ProductSku.php @@ -0,0 +1,45 @@ + + */ + protected array $_accessible = [ + 'product_id' => true, + 'sku' => true, + 'barcode' => true, + 'price' => true, + 'cost' => true, + 'created' => true, + 'modified' => true, + 'deleted' => true, + 'product' => false, + ]; +} diff --git a/src/Model/Table/ProductSkusTable.php b/src/Model/Table/ProductSkusTable.php new file mode 100644 index 0000000..463446f --- /dev/null +++ b/src/Model/Table/ProductSkusTable.php @@ -0,0 +1,114 @@ + newEntities(array $data, array $options = []) + * @method ProductSku get(mixed $primaryKey, array|string $finder = 'all', CacheInterface|string|null $cache = null, Closure|string|null $cacheKey = null, mixed ...$args) + * @method ProductSku findOrCreate($search, ?callable $callback = null, array $options = []) + * @method ProductSku patchEntity(EntityInterface $entity, array $data, array $options = []) + * @method array patchEntities(iterable $entities, array $data, array $options = []) + * @method ProductSku|false save(EntityInterface $entity, array $options = []) + * @method ProductSku saveOrFail(EntityInterface $entity, array $options = []) + * @method iterable|ResultSetInterface|false saveMany(iterable $entities, array $options = []) + * @method iterable|ResultSetInterface saveManyOrFail(iterable $entities, array $options = []) + * @method iterable|ResultSetInterface|false deleteMany(iterable $entities, array $options = []) + * @method iterable|ResultSetInterface deleteManyOrFail(iterable $entities, array $options = []) + * + * @mixin TimestampBehavior + */ +class ProductSkusTable extends Table +{ + /** + * Initialize method + * + * @param array $config The configuration for the Table. + * @return void + */ + public function initialize(array $config): void + { + parent::initialize($config); + + $this->setTable('product_skus'); + $this->setDisplayField('sku'); + $this->setPrimaryKey('id'); + + $this->addBehavior('Timestamp'); + + $this->belongsTo('Products', [ + 'foreignKey' => 'product_id', + 'joinType' => 'INNER', + ]); + } + + /** + * Default validation rules. + * + * @param Validator $validator Validator instance. + * @return Validator + */ + public function validationDefault(Validator $validator): Validator + { + $validator + ->uuid('product_id') + ->notEmptyString('product_id'); + + $validator + ->scalar('sku') + ->maxLength('sku', 255) + ->requirePresence('sku', 'create') + ->notEmptyString('sku'); + + $validator + ->scalar('barcode') + ->maxLength('barcode', 255) + ->allowEmptyString('barcode'); + + $validator + ->decimal('price') + ->allowEmptyString('price'); + + $validator + ->decimal('cost') + ->allowEmptyString('cost'); + + $validator + ->dateTime('deleted') + ->allowEmptyDateTime('deleted'); + + return $validator; + } + + /** + * Returns a rules checker object that will be used for validating + * application integrity. + * + * @param RulesChecker $rules The rules object to be modified. + * @return RulesChecker + */ + public function buildRules(RulesChecker $rules): RulesChecker + { + $rules->add($rules->existsIn(['product_id'], 'Products'), ['errorField' => 'product_id']); + + return $rules; + } +} diff --git a/templates/ProductSkus/add.php b/templates/ProductSkus/add.php new file mode 100644 index 0000000..8d9436f --- /dev/null +++ b/templates/ProductSkus/add.php @@ -0,0 +1,32 @@ + +
+ +
+
+ Form->create($productSku) ?> +
+ + Form->control('product_id'); + echo $this->Form->control('sku'); + echo $this->Form->control('barcode'); + echo $this->Form->control('price'); + echo $this->Form->control('cost'); + echo $this->Form->control('deleted', ['empty' => true]); + ?> +
+ Form->button(__('Submit')) ?> + Form->end() ?> +
+
+
diff --git a/templates/ProductSkus/edit.php b/templates/ProductSkus/edit.php new file mode 100644 index 0000000..6bd0cf2 --- /dev/null +++ b/templates/ProductSkus/edit.php @@ -0,0 +1,36 @@ + +
+ +
+
+ Form->create($productSku) ?> +
+ + Form->control('product_id'); + echo $this->Form->control('sku'); + echo $this->Form->control('barcode'); + echo $this->Form->control('price'); + echo $this->Form->control('cost'); + ?> +
+ Form->button(__('Submit')) ?> + Form->end() ?> +
+
+
diff --git a/templates/ProductSkus/index.php b/templates/ProductSkus/index.php new file mode 100644 index 0000000..a971c49 --- /dev/null +++ b/templates/ProductSkus/index.php @@ -0,0 +1,58 @@ + $productSkus + */ +?> +
+ Html->link(__('New Product Skus'), ['action' => 'add'], ['class' => 'button float-right']) ?> +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('id') ?>Paginator->sort('product_id') ?>Paginator->sort('sku') ?>Paginator->sort('barcode') ?>Paginator->sort('price') ?>Paginator->sort('cost') ?>Paginator->sort('created') ?>Paginator->sort('modified') ?>Paginator->sort('deleted') ?>
id) ?>product_id) ?>sku) ?>barcode) ?>price === null ? '' : $this->Number->format($productSku->price) ?>cost === null ? '' : $this->Number->format($productSku->cost) ?>created) ?>modified) ?>deleted) ?> + Html->link(__('View'), ['action' => 'view', $productSku->id]) ?> + Html->link(__('Edit'), ['action' => 'edit', $productSku->id]) ?> + Form->postLink(__('Delete'), ['action' => 'delete', $productSku->id], ['confirm' => __('Are you sure you want to delete # {0}?', $productSku->id)]) ?> +
+
+
+
    + Paginator->first('<< ' . __('first')) ?> + Paginator->prev('< ' . __('previous')) ?> + Paginator->numbers() ?> + Paginator->next(__('next') . ' >') ?> + Paginator->last(__('last') . ' >>') ?> +
+

Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?>

+
+
diff --git a/templates/ProductSkus/view.php b/templates/ProductSkus/view.php new file mode 100644 index 0000000..8a24070 --- /dev/null +++ b/templates/ProductSkus/view.php @@ -0,0 +1,60 @@ + +
+ +
+
+

sku) ?>

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
id) ?>
product_id) ?>
sku) ?>
barcode) ?>
price === null ? '' : $this->Number->format($productSku->price) ?>
cost === null ? '' : $this->Number->format($productSku->cost) ?>
created) ?>
modified) ?>
deleted) ?>
+
+
+
diff --git a/tests/Fixture/ProductSkusFixture.php b/tests/Fixture/ProductSkusFixture.php new file mode 100644 index 0000000..6bc872e --- /dev/null +++ b/tests/Fixture/ProductSkusFixture.php @@ -0,0 +1,41 @@ +records = [ + [ + 'id' => '3a477e3e-7977-4813-81f6-f85949613979', + 'product_id' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'sku' => '3a477e3e-7977-4813-81f6-f85949613979', + 'barcode' => '3a477e3e-7977-4813-81f6-f85949613979', + 'price' => 1.5, + 'cost' => 1.5, + 'created' => '2025-04-15 09:09:15', + 'modified' => '2025-04-15 09:09:15', + 'deleted' => null, + ], + ]; + parent::init(); + } +} diff --git a/tests/TestCase/Controller/ProductSkusControllerTest.php b/tests/TestCase/Controller/ProductSkusControllerTest.php new file mode 100644 index 0000000..8aa5947 --- /dev/null +++ b/tests/TestCase/Controller/ProductSkusControllerTest.php @@ -0,0 +1,326 @@ + + */ + protected array $fixtures = [ + 'plugin.CakeProducts.ProductSkus', + 'plugin.CakeProducts.Products', + 'plugin.CakeProducts.ProductAttributes', + ]; + + /** + * setUp method + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); +// $this->enableCsrfToken(); +// $this->enableSecurityToken(); + $config = $this->getTableLocator()->exists('ProductSkus') ? [] : ['className' => ProductSkusTable::class]; + $this->ProductSkus = $this->getTableLocator()->get('ProductSkus', $config); + } + + /** + * tearDown method + * + * @return void + */ + protected function tearDown(): void + { + unset($this->ProductSkus); + + parent::tearDown(); + } + + /** + * Test index method + * + * Tests the index action with a logged in user + * + * @uses \App\Controller\ProductSkusController::index() + * @throws Exception + * + * @return void + */ + public function testIndexGet(): void + { + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'index', + ]; + $this->get($url); + $this->assertResponseCode(200); + } + + /** + * Test view method + * + * Tests the view action with a logged in user + * + * @uses \App\Controller\ProductSkusController::view() + * @throws Exception + * + * @return void + */ + public function testViewGet(): void + { + $id = '3a477e3e-7977-4813-81f6-f85949613979'; + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'view', + $id, + ]; + $this->get($url); + $this->assertResponseCode(200); + } + + /** + * Test add method + * + * Tests the add action with a logged in user + * + * @uses \App\Controller\ProductSkusController::add() + * @throws Exception + * + * @return void + */ + public function testAddGet(): void + { + $cntBefore = $this->ProductSkus->find()->count(); + + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'add', + ]; + $this->get($url); + $this->assertResponseCode(200); + + $cntAfter = $this->ProductSkus->find()->count(); + $this->assertEquals($cntBefore, $cntAfter); + } + + /** + * Test add method + * + * Tests a POST request to the add action with a logged in user + * + * @uses \App\Controller\ProductSkusController::add() + * @throws Exception + * + * @return void + */ + public function testAddPostSuccess(): void + { + $cntBefore = $this->ProductSkus->find()->count(); + + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'add', + ]; + $data = [ + 'product_id' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'sku' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'barcode' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'price' => 1.5, + 'cost' => 1.5, + ]; + $this->post($url, $data); + $this->assertResponseCode(302); + $this->assertRedirectContains('product-skus'); + + $cntAfter = $this->ProductSkus->find()->count(); + $this->assertEquals($cntBefore + 1, $cntAfter); + } + + /** + * Test add method + * + * Tests a POST request to the add action with a logged in user + * + * @uses \App\Controller\ProductSkusController::add() + * @throws Exception + * + * @return void + */ + public function testAddPostFailure(): void + { + $cntBefore = $this->ProductSkus->find()->count(); + + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'add', + ]; + $data = [ + 'product_id' => '999999999', //does not exist + 'sku' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'barcode' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'price' => 1.5, + 'cost' => 1.5, + ]; + $this->post($url, $data); + $this->assertResponseCode(200); + + $cntAfter = $this->ProductSkus->find()->count(); + $this->assertEquals($cntBefore, $cntAfter); + } + + /** + * Test edit method + * + * Tests the edit action with a logged in user + * + * @uses \App\Controller\ProductSkusController::edit() + * @throws Exception + * + * @return void + */ + public function testEditGet(): void + { + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'edit', + '3a477e3e-7977-4813-81f6-f85949613979', + ]; + $this->get($url); + $this->assertResponseCode(200); + } + + /** + * Test edit method + * + * Tests a PUT request to the edit action with a logged in user + * + * @uses \App\Controller\ProductSkusController::edit() + * @throws Exception + * + * @return void + */ + public function testEditPutSuccess(): void + { + $this->loginUserByRole('admin'); + $id = '3a477e3e-7977-4813-81f6-f85949613979'; + $before = $this->ProductSkus->get($id); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'edit', + $id, + ]; + $data = [ + // test new data here + ]; + $this->put($url, $data); + + $this->assertResponseCode(302); + $this->assertRedirectContains('product-skus'); + + $after = $this->ProductSkus->get($id); + // assert saved properly below + } + + /** + * Test edit method + * + * Tests a PUT request to the edit action with a logged in user + * + * @uses \App\Controller\ProductSkusController::edit() + * @throws Exception + * + * @return void + */ + public function testEditPutFailure(): void + { + $this->loginUserByRole('admin'); + $id = '3a477e3e-7977-4813-81f6-f85949613979'; + $before = $this->ProductSkus->get($id); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'edit', + $id, + ]; + $data = [ + 'product_id' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'sku' => '', + 'barcode' => 'cfc98a9a-29b2-44c8-b587-8156adc05317', + 'price' => 1.5, + 'cost' => 1.5, + ]; + $this->put($url, $data); + $this->assertResponseCode(200); + $after = $this->ProductSkus->get($id); + + // assert save failed below + } + + /** + * Test delete method + * + * Tests the delete action with a logged in user + * + * @uses \App\Controller\ProductSkusController::delete() + * @throws Exception + * + * @return void + */ + public function testDelete(): void + { + $cntBefore = $this->ProductSkus->find()->count(); + + $this->loginUserByRole('admin'); + $url = [ + 'plugin' => 'CakeProducts', + 'controller' => 'ProductSkus', + 'action' => 'delete', + '3a477e3e-7977-4813-81f6-f85949613979', + ]; + $this->delete($url); + $this->assertResponseCode(302); + $this->assertRedirectContains('product-skus'); + + $cntAfter = $this->ProductSkus->find()->count(); + $this->assertEquals($cntBefore - 1, $cntAfter); + } +}