|
11 | 11 | use craft\commerce\db\Table; |
12 | 12 | use craft\commerce\elements\Product; |
13 | 13 | use craft\commerce\elements\Variant; |
| 14 | +use craft\commerce\Plugin; |
14 | 15 | use craft\db\Query; |
| 16 | +use craft\db\Table as CraftTable; |
15 | 17 | use craftcommercetests\fixtures\ProductFixture; |
16 | 18 | use DateTime; |
| 19 | +use ReflectionClass; |
17 | 20 |
|
18 | 21 | /** |
19 | 22 | * ProductTest |
@@ -357,4 +360,110 @@ public function testSaveProductAndVariants(): void |
357 | 360 | // Remove the product |
358 | 361 | \Craft::$app->getElements()->deleteElementById($product->id, Product::class, null, true); |
359 | 362 | } |
| 363 | + |
| 364 | + /** |
| 365 | + * @group Product |
| 366 | + */ |
| 367 | + public function testSkuFormatGeneratesSkuWhenEmpty(): void |
| 368 | + { |
| 369 | + $this->setProductTypeSkuFormat(2001, 'generated-sku-from-format'); |
| 370 | + |
| 371 | + $product = new Product(); |
| 372 | + $product->title = 'SKU Format Test Product'; |
| 373 | + $product->typeId = 2001; |
| 374 | + $product->enabled = false; |
| 375 | + |
| 376 | + $variant = new Variant(); |
| 377 | + $variant->title = 'Test Variant'; |
| 378 | + // SKU intentionally not set — should be generated from skuFormat |
| 379 | + |
| 380 | + $product->setVariants([$variant]); |
| 381 | + $product->validate(); |
| 382 | + |
| 383 | + self::assertEquals('generated-sku-from-format', $variant->sku); |
| 384 | + |
| 385 | + $this->setProductTypeSkuFormat(2001, null); |
| 386 | + } |
| 387 | + |
| 388 | + /** |
| 389 | + * @group Product |
| 390 | + */ |
| 391 | + public function testSkuFormatDeduplicatesWhenCollisionExists(): void |
| 392 | + { |
| 393 | + // Fixture variant 'rad-hood' already exists in the PURCHASABLES table. |
| 394 | + // Reset the sequence so the suffix is always predictable (-1). |
| 395 | + $this->resetSkuSequence('rad-hood'); |
| 396 | + $this->setProductTypeSkuFormat(2001, 'rad-hood'); |
| 397 | + |
| 398 | + $product = new Product(); |
| 399 | + $product->title = 'Collision Test Product'; |
| 400 | + $product->typeId = 2001; |
| 401 | + $product->enabled = false; |
| 402 | + |
| 403 | + $variant = new Variant(); |
| 404 | + $variant->title = 'Test Variant'; |
| 405 | + // No SKU — format generates 'rad-hood' which collides with the fixture variant |
| 406 | + |
| 407 | + $product->setVariants([$variant]); |
| 408 | + $product->validate(); |
| 409 | + |
| 410 | + self::assertEquals('rad-hood-1', $variant->sku); |
| 411 | + |
| 412 | + $this->setProductTypeSkuFormat(2001, null); |
| 413 | + } |
| 414 | + |
| 415 | + /** |
| 416 | + * @group Product |
| 417 | + */ |
| 418 | + public function testSkuFormatDeduplicatesMultipleVariantsWithCollision(): void |
| 419 | + { |
| 420 | + // Fixture variant 'hct-white' already exists in the PURCHASABLES table. |
| 421 | + // Reset the sequence so suffixes are always predictable (-1, -2). |
| 422 | + $this->resetSkuSequence('hct-white'); |
| 423 | + $this->setProductTypeSkuFormat(2001, 'hct-white'); |
| 424 | + |
| 425 | + $product = new Product(); |
| 426 | + $product->title = 'Multi-Variant Collision Test'; |
| 427 | + $product->typeId = 2001; |
| 428 | + $product->enabled = false; |
| 429 | + |
| 430 | + $variant1 = new Variant(); |
| 431 | + $variant1->title = 'Variant One'; |
| 432 | + |
| 433 | + $variant2 = new Variant(); |
| 434 | + $variant2->title = 'Variant Two'; |
| 435 | + |
| 436 | + $product->setVariants([$variant1, $variant2]); |
| 437 | + $product->validate(); |
| 438 | + |
| 439 | + self::assertEquals('hct-white-1', $variant1->sku); |
| 440 | + self::assertEquals('hct-white-2', $variant2->sku); |
| 441 | + |
| 442 | + $this->setProductTypeSkuFormat(2001, null); |
| 443 | + } |
| 444 | + |
| 445 | + private function resetSkuSequence(string $baseSku): void |
| 446 | + { |
| 447 | + \Craft::$app->getDb()->createCommand() |
| 448 | + ->delete(CraftTable::SEQUENCES, ['name' => 'sku::' . $baseSku]) |
| 449 | + ->execute(); |
| 450 | + } |
| 451 | + |
| 452 | + private function setProductTypeSkuFormat(int $typeId, ?string $skuFormat): void |
| 453 | + { |
| 454 | + $productTypesService = Plugin::getInstance()->getProductTypes(); |
| 455 | + // Ensure types are loaded into cache |
| 456 | + $productTypesService->getAllProductTypes(); |
| 457 | + |
| 458 | + $reflection = new ReflectionClass($productTypesService); |
| 459 | + $prop = $reflection->getProperty('_allProductTypes'); |
| 460 | + $prop->setAccessible(true); |
| 461 | + |
| 462 | + foreach ($prop->getValue($productTypesService) as $type) { |
| 463 | + if ($type->id === $typeId) { |
| 464 | + $type->skuFormat = $skuFormat; |
| 465 | + return; |
| 466 | + } |
| 467 | + } |
| 468 | + } |
360 | 469 | } |
0 commit comments