Align commerce module with the monorepo module structure by updating all namespaces to use the Core\Mod\Commerce convention. This change supports the recent monorepo separation and ensures consistency with other modules. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
236 lines
8 KiB
PHP
236 lines
8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Core\Mod\Commerce\Data\BundleItem;
|
|
use Core\Mod\Commerce\Data\ParsedItem;
|
|
use Core\Mod\Commerce\Data\SkuOption;
|
|
use Core\Mod\Commerce\Services\SkuBuilderService;
|
|
use Core\Mod\Commerce\Services\SkuParserService;
|
|
|
|
describe('Compound SKU Parser', function () {
|
|
beforeEach(function () {
|
|
$this->parser = app(SkuParserService::class);
|
|
});
|
|
|
|
test('parses simple SKU without options', function () {
|
|
$result = $this->parser->parse('LAPTOP');
|
|
|
|
expect($result->count())->toBe(1);
|
|
expect($result->items[0])->toBeInstanceOf(ParsedItem::class);
|
|
expect($result->items[0]->baseSku)->toBe('LAPTOP');
|
|
expect($result->items[0]->options)->toBeEmpty();
|
|
});
|
|
|
|
test('parses SKU with single option', function () {
|
|
$result = $this->parser->parse('LAPTOP-ram~16gb');
|
|
|
|
expect($result->count())->toBe(1);
|
|
expect($result->items[0]->baseSku)->toBe('LAPTOP');
|
|
expect($result->items[0]->options)->toHaveCount(1);
|
|
expect($result->items[0]->options[0]->code)->toBe('ram');
|
|
expect($result->items[0]->options[0]->value)->toBe('16gb');
|
|
expect($result->items[0]->options[0]->quantity)->toBe(1);
|
|
});
|
|
|
|
test('parses SKU with multiple options', function () {
|
|
$result = $this->parser->parse('LAPTOP-ram~16gb-ssd~512gb-color~silver');
|
|
|
|
expect($result->count())->toBe(1);
|
|
expect($result->items[0]->options)->toHaveCount(3);
|
|
expect($result->items[0]->getOption('ram')->value)->toBe('16gb');
|
|
expect($result->items[0]->getOption('ssd')->value)->toBe('512gb');
|
|
expect($result->items[0]->getOption('color')->value)->toBe('silver');
|
|
});
|
|
|
|
test('parses option with quantity', function () {
|
|
$result = $this->parser->parse('LAPTOP-cover~black*2');
|
|
|
|
expect($result->items[0]->options[0]->code)->toBe('cover');
|
|
expect($result->items[0]->options[0]->value)->toBe('black');
|
|
expect($result->items[0]->options[0]->quantity)->toBe(2);
|
|
});
|
|
|
|
test('parses multiple comma-separated items', function () {
|
|
$result = $this->parser->parse('LAPTOP-ram~16gb,MOUSE,PAD-size~xl');
|
|
|
|
expect($result->count())->toBe(3);
|
|
expect($result->items[0]->baseSku)->toBe('LAPTOP');
|
|
expect($result->items[1]->baseSku)->toBe('MOUSE');
|
|
expect($result->items[2]->baseSku)->toBe('PAD');
|
|
expect($result->hasBundles())->toBeFalse();
|
|
});
|
|
|
|
test('parses pipe-separated bundle', function () {
|
|
$result = $this->parser->parse('LAPTOP-ram~16gb|MOUSE|PAD');
|
|
|
|
expect($result->count())->toBe(1);
|
|
expect($result->hasBundles())->toBeTrue();
|
|
expect($result->items[0])->toBeInstanceOf(BundleItem::class);
|
|
|
|
$bundle = $result->items[0];
|
|
expect($bundle->count())->toBe(3);
|
|
expect($bundle->getBaseSkus())->toBe(['LAPTOP', 'MOUSE', 'PAD']);
|
|
});
|
|
|
|
test('bundle hash is consistent regardless of order', function () {
|
|
$result1 = $this->parser->parse('LAPTOP|MOUSE|PAD');
|
|
$result2 = $this->parser->parse('PAD|LAPTOP|MOUSE');
|
|
|
|
expect($result1->items[0]->hash)->toBe($result2->items[0]->hash);
|
|
});
|
|
|
|
test('parses mixed bundles and singles', function () {
|
|
$result = $this->parser->parse('LAPTOP|MOUSE,HDMI-length~2m');
|
|
|
|
expect($result->count())->toBe(2);
|
|
expect($result->items[0])->toBeInstanceOf(BundleItem::class);
|
|
expect($result->items[1])->toBeInstanceOf(ParsedItem::class);
|
|
});
|
|
|
|
test('preserves entity lineage in base SKU', function () {
|
|
// SKU with lineage: ORGORG-WBUTS-PROD500
|
|
$result = $this->parser->parse('ORGORG-WBUTS-PROD500-ram~16gb');
|
|
|
|
expect($result->items[0]->baseSku)->toBe('ORGORG-WBUTS-PROD500');
|
|
expect($result->items[0]->options)->toHaveCount(1);
|
|
});
|
|
|
|
test('validates SKU format', function () {
|
|
$valid = $this->parser->validate('LAPTOP-ram~16gb');
|
|
expect($valid['valid'])->toBeTrue();
|
|
|
|
$invalid = $this->parser->validate('');
|
|
expect($invalid['valid'])->toBeFalse();
|
|
});
|
|
|
|
test('round trips through parse and toString', function () {
|
|
$original = 'LAPTOP-ram~16gb-ssd~512gb';
|
|
$result = $this->parser->parse($original);
|
|
|
|
expect($result->toString())->toBe($original);
|
|
});
|
|
});
|
|
|
|
describe('Compound SKU Builder', function () {
|
|
beforeEach(function () {
|
|
$this->builder = app(SkuBuilderService::class);
|
|
});
|
|
|
|
test('builds simple item', function () {
|
|
$sku = $this->builder->build([
|
|
['base_sku' => 'laptop'],
|
|
]);
|
|
|
|
expect($sku)->toBe('LAPTOP');
|
|
});
|
|
|
|
test('builds item with options', function () {
|
|
$sku = $this->builder->build([
|
|
[
|
|
'base_sku' => 'laptop',
|
|
'options' => [
|
|
['code' => 'ram', 'value' => '16gb'],
|
|
['code' => 'ssd', 'value' => '512gb'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
expect($sku)->toBe('LAPTOP-ram~16gb-ssd~512gb');
|
|
});
|
|
|
|
test('builds item with quantity on option', function () {
|
|
$sku = $this->builder->build([
|
|
[
|
|
'base_sku' => 'laptop',
|
|
'options' => [
|
|
['code' => 'cover', 'value' => 'black', 'quantity' => 2],
|
|
],
|
|
],
|
|
]);
|
|
|
|
expect($sku)->toBe('LAPTOP-cover~black*2');
|
|
});
|
|
|
|
test('builds multiple items comma-separated', function () {
|
|
$sku = $this->builder->build([
|
|
['base_sku' => 'laptop'],
|
|
['base_sku' => 'mouse'],
|
|
]);
|
|
|
|
expect($sku)->toBe('LAPTOP,MOUSE');
|
|
});
|
|
|
|
test('builds bundle with same group', function () {
|
|
$sku = $this->builder->build([
|
|
['base_sku' => 'laptop', 'bundle_group' => 'cyber'],
|
|
['base_sku' => 'mouse', 'bundle_group' => 'cyber'],
|
|
['base_sku' => 'hdmi'], // standalone
|
|
]);
|
|
|
|
expect($sku)->toBe('LAPTOP|MOUSE,HDMI');
|
|
});
|
|
|
|
test('adds entity lineage', function () {
|
|
$sku = $this->builder->addLineage('PROD500', ['ORGORG', 'WBUTS']);
|
|
|
|
expect($sku)->toBe('ORGORG-WBUTS-PROD500');
|
|
});
|
|
|
|
test('builds with lineage', function () {
|
|
$sku = $this->builder->buildWithLineage(
|
|
['ORGORG', 'WBUTS'],
|
|
[
|
|
[
|
|
'base_sku' => 'PROD500',
|
|
'options' => [['code' => 'size', 'value' => 'xl']],
|
|
],
|
|
]
|
|
);
|
|
|
|
expect($sku)->toBe('ORGORG-WBUTS-PROD500-size~xl');
|
|
});
|
|
|
|
test('generates bundle hash', function () {
|
|
$hash = $this->builder->generateBundleHash(['laptop', 'mouse', 'pad']);
|
|
|
|
// Hash is deterministic
|
|
expect($hash)->toBe($this->builder->generateBundleHash(['PAD', 'LAPTOP', 'MOUSE']));
|
|
expect(strlen($hash))->toBe(64); // SHA256
|
|
});
|
|
|
|
test('creates option and item helpers', function () {
|
|
$option = $this->builder->option('ram', '16gb', 2);
|
|
expect($option)->toBeInstanceOf(SkuOption::class);
|
|
expect($option->toString())->toBe('ram~16gb*2');
|
|
|
|
$item = $this->builder->item('LAPTOP', [$option]);
|
|
expect($item)->toBeInstanceOf(ParsedItem::class);
|
|
expect($item->toString())->toBe('LAPTOP-ram~16gb*2');
|
|
});
|
|
});
|
|
|
|
describe('SKU Parse Result', function () {
|
|
test('collects all base SKUs', function () {
|
|
$parser = app(SkuParserService::class);
|
|
$result = $parser->parse('LAPTOP|MOUSE,HDMI,PAD');
|
|
|
|
expect($result->getAllBaseSkus())->toBe(['LAPTOP', 'MOUSE', 'HDMI', 'PAD']);
|
|
});
|
|
|
|
test('counts products correctly', function () {
|
|
$parser = app(SkuParserService::class);
|
|
$result = $parser->parse('LAPTOP|MOUSE|PAD,HDMI');
|
|
|
|
expect($result->count())->toBe(2); // 1 bundle + 1 single
|
|
expect($result->productCount())->toBe(4); // 3 in bundle + 1 single
|
|
});
|
|
|
|
test('extracts bundle hashes', function () {
|
|
$parser = app(SkuParserService::class);
|
|
$result = $parser->parse('LAPTOP|MOUSE,KEYBOARD|PAD');
|
|
|
|
$hashes = $result->getBundleHashes();
|
|
expect($hashes)->toHaveCount(2);
|
|
});
|
|
});
|