specs/RFC-006-COMPOUND-SKU.md
2026-02-01 07:41:21 +00:00

258 lines
7.4 KiB
Markdown

# RFC: Compound SKU Format
**Status:** Implemented
**Created:** 2026-01-15
**Authors:** Host UK Engineering
---
## Abstract
The Compound SKU format encodes product identity, options, quantities, and bundle groupings in a single parseable string. Like [HLCRF](./HLCRF-COMPOSITOR.md) for layouts, it makes complex structure a portable, self-describing data type.
One scan tells you everything. No lookups. No mistakes. One barcode = complete fulfillment knowledge.
---
## Format Specification
```
SKU-<opt>~<val>*<qty>[-<opt>~<val>*<qty>]...
```
| Symbol | Purpose | Example |
|--------|---------|----------------------|
| `-` | Option separator | `LAPTOP-ram~16gb` |
| `~` | Value indicator | `ram~16gb` |
| `*` | Quantity indicator | `cover~black*2` |
| `,` | Item separator | `LAPTOP,MOUSE,PAD` |
| `\|` | Bundle separator | `LAPTOP\|MOUSE\|PAD` |
---
## Examples
### Single product with options
```
LAPTOP-ram~16gb-ssd~512gb-color~silver
```
### Option with quantity
```
LAPTOP-ram~16gb-cover~black*2
```
Two black covers included.
### Multiple separate items
```
LAPTOP-ram~16gb,HDMI-length~2m,MOUSE-color~black
```
Comma separates distinct line items.
### Bundle (discount lookup)
```
LAPTOP-ram~16gb\|MOUSE-color~black\|PAD-size~xl
```
Pipe binds items for bundle discount detection.
### With entity lineage
```
ORGORG-WBUTS-PROD500-ram~16gb
│ │ │ └── Option
│ │ └────────── Base product SKU
│ └──────────────── M2 entity code
└─────────────────────── M1 entity code
```
The lineage prefix traces through the entity hierarchy.
---
## Bundle Discount Detection
When a compound SKU contains `|` (bundle separator):
```
┌──────────────────────────────────────────────────────────────┐
│ Input: LAPTOP-ram~16gb|MOUSE-color~black|PAD-size~xl │
│ │
│ Step 1: Detect Bundle (found |) │
│ │
│ Step 2: Strip Human Choices │
│ → LAPTOP|MOUSE|PAD │
│ │
│ Step 3: Hash the Raw Combination │
│ → hash("LAPTOP|MOUSE|PAD") = "abc123..." │
│ │
│ Step 4: Lookup Bundle Discount │
│ → commerce_bundle_hashes["abc123"] = 20% off │
│ │
│ Step 5: Apply Discount │
│ → Bundle price calculated │
└──────────────────────────────────────────────────────────────┘
```
The hash is computed from **sorted base SKUs** (stripping options), so `LAPTOP|MOUSE|PAD` and `PAD|LAPTOP|MOUSE` produce the same hash.
---
## API Reference
### SkuParserService
Parses compound SKU strings into structured data.
```php
use Mod\Commerce\Services\SkuParserService;
$parser = app(SkuParserService::class);
// Parse a compound SKU
$result = $parser->parse('LAPTOP-ram~16gb|MOUSE,HDMI');
// Result contains ParsedItem and BundleItem objects
$result->count(); // 2 (1 bundle + 1 single)
$result->productCount(); // 4 (3 in bundle + 1 single)
$result->hasBundles(); // true
$result->getBundleHashes(); // ['abc123...']
$result->getAllBaseSkus(); // ['LAPTOP', 'MOUSE', 'HDMI']
// Access items
foreach ($result->items as $item) {
if ($item instanceof BundleItem) {
echo "Bundle: " . $item->getBaseSkuString();
} else {
echo "Item: " . $item->baseSku;
}
}
```
### SkuBuilderService
Builds compound SKU strings from structured data.
```php
use Mod\Commerce\Services\SkuBuilderService;
$builder = app(SkuBuilderService::class);
// Build from line items
$sku = $builder->build([
[
'base_sku' => 'laptop',
'options' => [
['code' => 'ram', 'value' => '16gb'],
['code' => 'ssd', 'value' => '512gb'],
],
'bundle_group' => 'cyber', // Groups into bundle
],
[
'base_sku' => 'mouse',
'bundle_group' => 'cyber',
],
[
'base_sku' => 'hdmi', // No group = standalone
],
]);
// Returns: "LAPTOP-ram~16gb-ssd~512gb|MOUSE,HDMI"
// Add entity lineage
$sku = $builder->addLineage('PROD500', ['ORGORG', 'WBUTS']);
// Returns: "ORGORG-WBUTS-PROD500"
// Generate bundle hash for discount creation
$hash = $builder->generateBundleHash(['LAPTOP', 'MOUSE', 'PAD']);
```
### Data Transfer Objects
```php
use Mod\Commerce\Data\SkuOption;
use Mod\Commerce\Data\ParsedItem;
use Mod\Commerce\Data\BundleItem;
use Mod\Commerce\Data\SkuParseResult;
// Option: code~value*quantity
$option = new SkuOption('ram', '16gb', 1);
$option->toString(); // "ram~16gb"
// Item: baseSku with options
$item = new ParsedItem('LAPTOP', [$option]);
$item->toString(); // "LAPTOP-ram~16gb"
$item->getOption('ram'); // SkuOption
$item->hasOption('ssd'); // false
// Bundle: items grouped for discount
$bundle = new BundleItem($items, $hash);
$bundle->getBaseSkus(); // ['LAPTOP', 'MOUSE']
$bundle->getBaseSkuString(); // "LAPTOP|MOUSE"
$bundle->containsSku('MOUSE'); // true
```
---
## Database Schema
### Bundle Hash Table
```sql
CREATE TABLE commerce_bundle_hashes (
id BIGINT PRIMARY KEY,
hash VARCHAR(64) UNIQUE, -- SHA256 of sorted base SKUs
base_skus VARCHAR(512), -- "LAPTOP|MOUSE|PAD" (debugging)
-- Discount (one of these)
coupon_code VARCHAR(64),
fixed_price DECIMAL(12,2),
discount_percent DECIMAL(5,2),
discount_amount DECIMAL(12,2),
entity_id BIGINT, -- Scope to M1/M2/M3
valid_from TIMESTAMP,
valid_until TIMESTAMP,
active BOOLEAN DEFAULT TRUE
);
```
---
## Connection to HLCRF
Both Compound SKU and HLCRF share the same core innovation: **hierarchy encoded in a parseable string**.
| System | String | Meaning |
|--------|--------|---------|
| HLCRF | `H[LCR]CF` | Layout with nested body in header |
| SKU | `ORGORG-WBUTS-PROD-ram~16gb` | Product with entity lineage and option |
Both eliminate database lookups by making structure self-describing. Parse the string, get the full picture.
---
## Implementation Files
- `app/Mod/Commerce/Services/SkuParserService.php` — Parser
- `app/Mod/Commerce/Services/SkuBuilderService.php` — Builder
- `app/Mod/Commerce/Services/SkuLineageService.php` — Entity lineage tracking
- `app/Mod/Commerce/Data/SkuOption.php` — Option DTO
- `app/Mod/Commerce/Data/ParsedItem.php` — Item DTO
- `app/Mod/Commerce/Data/BundleItem.php` — Bundle DTO
- `app/Mod/Commerce/Data/SkuParseResult.php` — Parse result DTO
- `app/Mod/Commerce/Models/BundleHash.php` — Bundle discount model
- `app/Mod/Commerce/Tests/Feature/CompoundSkuTest.php` — Tests
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-01-15 | Initial RFC |