Skip to content

Instantly share code, notes, and snippets.

@jstoone
Created January 28, 2026 13:47
Show Gist options
  • Select an option

  • Save jstoone/19210d3015d34335b700d6e0a08d3ece to your computer and use it in GitHub Desktop.

Select an option

Save jstoone/19210d3015d34335b700d6e0a08d3ece to your computer and use it in GitHub Desktop.
A new way of testing, using Journeys
<?php
use App\Domain\PIM\Events\ProductCreated;
use App\Domain\PIM\Events\ProductSplitCreated;
use App\Domain\PIM\Events\VariantChangedOptionSelections;
use App\Domain\PIM\Events\VariantCreated;
use App\Domain\PIM\Events\VariantDeleted;
use App\Domain\PIM\Events\VariantOptionAssigned;
use App\Domain\PIM\Events\VariantOptionCreated;
use App\Domain\PIM\Events\VariantOptionValueAdded;
use App\Domain\PIM\Events\VariantOptionWasSplit;
use App\Domain\PIM\Events\VariantOptionWasUnsplit;
use App\Domain\PIM\States\ProductSplitState;
use App\Domain\PIM\States\ProductState;
use App\Domain\PIM\States\VariantOptionState;
use App\Domain\PIM\States\VariantState;
use App\Domain\Platform\Factories\OrganizationFactory;
use App\Models\PIM\ProductSplit;
use App\Models\PIM\Variant;
use App\Models\VerbEvent;
describe('The journey of That One Drink and how it splits', function () {
it('can complete a full story arc', function () {
// We're launching "That One Drink" - a simple canned beverage.
// Starting with just one product and one variant (the default).
$organization = OrganizationFactory::new()
->withLocale('en')
->create();
$product = verb(new ProductCreated(
organization_id: $organization->id,
handle: 'that-one-drink',
name: 'That One Drink',
))->state(ProductState::class);
$productId = $product->id;
$variant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [],
sku: 'DRINK-001',
name: 'Default',
))->state(VariantState::class);
// A default split should be automatically created for the product
$splitCreatedEvent = VerbEvent::type(ProductSplitCreated::class)->sole();
expect($splitCreatedEvent)->not->toBeNull();
$event = $splitCreatedEvent->event();
expect($event->product_id)->toBe($product->id)
->and($event->is_default)->toBe(true)
->and($event->option_selections)->toBe([]);
// Success! The drink is selling well. Customer feedback is pouring in.
// The #1 request? A Kiwi flavor! Time to expand the product line.
$flavorOption = verb(new VariantOptionCreated(
organization_id: $organization->id,
option_name: 'Flavor',
option_key: 'flavor',
))->state(VariantOptionState::class);
verb(new VariantOptionValueAdded(
organization_id: $organization->id,
option_id: $flavorOption->id,
value_key: 'original',
value_name: 'Original',
));
verb(new VariantOptionValueAdded(
organization_id: $organization->id,
option_id: $flavorOption->id,
value_key: 'kiwi',
value_name: 'Kiwi',
));
// Assign the Flavor option to our product
verb(new VariantOptionAssigned(
product_id: $product->id,
option_id: $flavorOption->id,
));
// Now we need to update our existing variant to specify it's the Original flavor.
// This keeps our catalog organized as we add the new Kiwi variant.
verb(new VariantChangedOptionSelections(
organization_id: $organization->id,
variant_id: $variant->id,
option_selections: [
$flavorOption->id => 'original',
],
));
// The variant should now have the flavor selection
$variant->fresh();
expect($variant->option_selections)->toBe([
$flavorOption->id => 'original',
]);
// Launch day for Kiwi! Let's create the new variant.
$kiwiVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [
$flavorOption->id => 'kiwi',
],
sku: 'DRINK-002',
name: 'Kiwi',
))->state(VariantState::class);
$product->fresh();
// We should still only have one split (the default split)
expect($product->splits)->toHaveCount(1);
// Load the default split and verify it contains both variants
$defaultSplitId = array_values($product->splits)[0];
$defaultSplit = ProductSplitState::load($defaultSplitId);
expect($defaultSplit->is_default)->toBe(true);
expect(ProductSplit::where('product_id', $productId)->count())->toBe(1);
$defaultSplitProjection = ProductSplit::where('product_id', $productId)->first();
expect($defaultSplitProjection)->not->toBeNull();
expect($defaultSplitProjection->is_default)->toBeTrue();
// Verify variant's split_id is set correctly in projection
$kiwiVariantProjection = Variant::find($kiwiVariant->id);
expect($kiwiVariantProjection->split_id)->toBe($defaultSplitId);
// Marketing has been tracking the data - Kiwi is a hit!
// But it's hard to deep-link or make specific ads because it's one click away.
// They need Original and Kiwi to be separate products for better targeting.
// Time to use the split function! We'll enable splitting on the Flavor option itself.
// Capture the default split ID before subdividing
$defaultSplitId = $product->splits['default'];
verb(new VariantOptionWasSplit(
organization_id: $organization->id,
option_id: $flavorOption->id,
));
$organization->fresh();
// Now we should have two splits: Original and Kiwi
$product->fresh();
expect($product->splits)->toHaveCount(2);
// The default split key should no longer exist
expect($product->splits)->not->toHaveKey('default');
// Find the Original and Kiwi splits
$flavorOptionId = $flavorOption->id;
$originalSplitKey = $flavorOptionId . ':original';
$kiwiSplitKey = $flavorOptionId . ':kiwi';
expect($product->splits)->toHaveKey($originalSplitKey)
->and($product->splits)->toHaveKey($kiwiSplitKey);
$originalSplit = ProductSplitState::load($product->splits[$originalSplitKey]);
$kiwiSplit = ProductSplitState::load($product->splits[$kiwiSplitKey]);
// The Original split should have reused the default split ID (first value in option->values)
expect($originalSplit->id)->toBe($defaultSplitId)
->and($originalSplit->name)->toBe('Original')
->and($originalSplit->is_default)->toBe(false)
->and($originalSplit->option_selections)->toBe([$flavorOption->id => 'original']);
// Kiwi split should be a new split
expect($kiwiSplit->id)->not->toBe($defaultSplitId)
->and($kiwiSplit->name)->toBe('Kiwi')
->and($kiwiSplit->is_default)->toBe(false)
->and($kiwiSplit->option_selections)->toBe([$flavorOption->id => 'kiwi']);
// Verify variant projections have correct split_id
expect(Variant::find($variant->id)->split_id)->toBe($originalSplit->id);
expect(Variant::find($kiwiVariant->id)->split_id)->toBe($kiwiSplit->id);
$flavorSplits = ProductSplit::where('product_id', $productId)
->orderBy('path_key')
->get();
expect($flavorSplits)->toHaveCount(2)
->and($flavorSplits->pluck('path_key')->all())->toBe([
$kiwiSplitKey,
$originalSplitKey,
]);
// Next chapter: we are now allowing to sell cans and glass bottles
// - Create “Container” variant option
// - Add to product
// - Assign current Original + Kiwi variants to “can”
// - Create Original / Glass
// - Create Kiwi / Glass
// - Assert that the two existing splits now have 2 variants
$containerOption = verb(new VariantOptionCreated(
organization_id: $organization->id,
option_name: 'Container',
option_key: 'container',
))->state(VariantOptionState::class);
verb(new VariantOptionValueAdded(
organization_id: $organization->id,
option_id: $containerOption->id,
value_key: 'can',
value_name: 'Can',
));
verb(new VariantOptionValueAdded(
organization_id: $organization->id,
option_id: $containerOption->id,
value_key: 'glass',
value_name: 'Glass',
));
verb(new VariantOptionAssigned(
product_id: $product->id,
option_id: $containerOption->id,
));
// Existing variants become canned drinks
verb(new VariantChangedOptionSelections(
organization_id: $organization->id,
variant_id: $variant->id,
option_selections: [
$flavorOption->id => 'original',
$containerOption->id => 'can',
],
));
verb(new VariantChangedOptionSelections(
organization_id: $organization->id,
variant_id: $kiwiVariant->id,
option_selections: [
$flavorOption->id => 'kiwi',
$containerOption->id => 'can',
],
));
// Launch glass variants for both flavors
$originalGlassVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [
$flavorOption->id => 'original',
$containerOption->id => 'glass',
],
sku: 'DRINK-003',
name: 'Original Glass',
))->state(VariantState::class);
$kiwiGlassVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [
$flavorOption->id => 'kiwi',
$containerOption->id => 'glass',
],
sku: 'DRINK-004',
name: 'Kiwi Glass',
))->state(VariantState::class);
$product->fresh();
$originalSplit->fresh();
$kiwiSplit->fresh();
// Verify variant projections have correct split_id
expect(Variant::find($variant->id)->split_id)->toBe($originalSplit->id);
expect(Variant::find($originalGlassVariant->id)->split_id)->toBe($originalSplit->id);
expect(Variant::find($kiwiVariant->id)->split_id)->toBe($kiwiSplit->id);
expect(Variant::find($kiwiGlassVariant->id)->split_id)->toBe($kiwiSplit->id);
// Next chapter: we are ready to create branding pages for cans and glass products
// - Split “Container” variant option
// - Assert it cascaded into four splits with one variant each
verb(new VariantOptionWasSplit(
organization_id: $organization->id,
option_id: $containerOption->id,
));
$product->fresh();
$containerOptionId = $containerOption->id;
$originalCanKey = $flavorOptionId . ':original|' . $containerOptionId . ':can';
$originalGlassKey = $flavorOptionId . ':original|' . $containerOptionId . ':glass';
$kiwiCanKey = $flavorOptionId . ':kiwi|' . $containerOptionId . ':can';
$kiwiGlassKey = $flavorOptionId . ':kiwi|' . $containerOptionId . ':glass';
expect($product->splits)->toHaveCount(4);
expect($product->splits)->toHaveKey($originalCanKey)
->and($product->splits)->toHaveKey($originalGlassKey)
->and($product->splits)->toHaveKey($kiwiCanKey)
->and($product->splits)->toHaveKey($kiwiGlassKey);
$originalSplit->fresh();
$kiwiSplit->fresh();
// Existing split IDs should now represent the canned variants
expect($product->splits[$originalCanKey])->toBe($originalSplit->id);
expect($product->splits[$kiwiCanKey])->toBe($kiwiSplit->id);
expect($originalSplit->path_key)->toBe($originalCanKey)
->and($originalSplit->name)->toBe('Original / Can');
expect($kiwiSplit->path_key)->toBe($kiwiCanKey)
->and($kiwiSplit->name)->toBe('Kiwi / Can');
$originalGlassSplit = ProductSplitState::load($product->splits[$originalGlassKey]);
$kiwiGlassSplit = ProductSplitState::load($product->splits[$kiwiGlassKey]);
expect($originalGlassSplit->name)->toBe('Original / Glass');
expect($kiwiGlassSplit->name)->toBe('Kiwi / Glass');
// Verify variant projections have correct split_id
expect(Variant::find($variant->id)->split_id)->toBe($originalSplit->id);
expect(Variant::find($originalGlassVariant->id)->split_id)->toBe($originalGlassSplit->id);
expect(Variant::find($kiwiVariant->id)->split_id)->toBe($kiwiSplit->id);
expect(Variant::find($kiwiGlassVariant->id)->split_id)->toBe($kiwiGlassSplit->id);
$containerKeys = [
$originalCanKey,
$originalGlassKey,
$kiwiCanKey,
$kiwiGlassKey,
];
$containerSplits = ProductSplit::where('product_id', $productId)
->whereIn('path_key', $containerKeys)
->with('variants')
->get()
->keyBy('path_key');
expect($containerSplits)->toHaveCount(4);
expect($containerSplits[$originalGlassKey]->variants->pluck('id')->all())
->toBe([$originalGlassVariant->id]);
expect($containerSplits[$kiwiGlassKey]->variants->pluck('id')->all())
->toBe([$kiwiGlassVariant->id]);
// Campaign time: "Vote For That One New Drink" pushes an Orange flavour
// - Add new flavor value
// - Launch Orange Can + Orange Glass variants
// - Expect new splits (flavor:orange with both containers) to be created lazily and contain the new variants
verb(new VariantOptionValueAdded(
organization_id: $organization->id,
option_id: $flavorOption->id,
value_key: 'orange',
value_name: 'Orange',
));
$orangeCanVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [
$flavorOption->id => 'orange',
$containerOption->id => 'can',
],
sku: 'DRINK-005',
name: 'Orange Can',
))->state(VariantState::class);
$orangeGlassVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $product->id,
option_selections: [
$flavorOption->id => 'orange',
$containerOption->id => 'glass',
],
sku: 'DRINK-006',
name: 'Orange Glass',
))->state(VariantState::class);
$product->fresh();
$orangeCanKey = $flavorOption->id . ':orange|' . $containerOption->id . ':can';
$orangeGlassKey = $flavorOption->id . ':orange|' . $containerOption->id . ':glass';
expect($product->splits)->toHaveKey($orangeCanKey)
->and($product->splits)->toHaveKey($orangeGlassKey);
$orangeCanSplit = ProductSplitState::load($product->splits[$orangeCanKey]);
$orangeGlassSplit = ProductSplitState::load($product->splits[$orangeGlassKey]);
expect($orangeCanSplit->name)->toBe('Orange / Can');
expect($orangeGlassSplit->name)->toBe('Orange / Glass');
// Verify variant projections have correct split_id
expect(Variant::find($orangeCanVariant->id)->split_id)->toBe($orangeCanSplit->id);
expect(Variant::find($orangeGlassVariant->id)->split_id)->toBe($orangeGlassSplit->id);
// Plot twist: Orange is a massive success, but Kiwi sales have dropped to zero.
// The data is clear - customers prefer Orange. Time to discontinue Kiwi.
// We need to delete the Kiwi variants. When all variants in a split are deleted,
// the split should be automatically removed from the product.
// Delete Kiwi Can variant
verb(new VariantDeleted(
organization_id: $organization->id,
variant_id: $kiwiVariant->id,
));
// Delete Kiwi Glass variant
verb(new VariantDeleted(
organization_id: $organization->id,
variant_id: $kiwiGlassVariant->id,
));
// Verify variant states are marked as deleted
$kiwiVariant->fresh();
$kiwiGlassVariant->fresh();
expect($kiwiVariant->deleted_at)->not->toBeNull();
expect($kiwiGlassVariant->deleted_at)->not->toBeNull();
// Verify variants are deleted from DB
expect(Variant::find($kiwiVariant->id))->toBeNull();
expect(Variant::find($kiwiGlassVariant->id))->toBeNull();
// Empty splits persist (users can manually delete, won't pass Sync spec)
$product->fresh();
expect($product->splits)->toHaveKey($kiwiCanKey);
expect($product->splits)->toHaveKey($kiwiGlassKey);
// Product has 6 splits - 4 with variants, 2 empty (Kiwi)
expect($product->splits)->toHaveCount(6);
expect($product->splits)->toHaveKey($originalCanKey);
expect($product->splits)->toHaveKey($originalGlassKey);
expect($product->splits)->toHaveKey($orangeCanKey);
expect($product->splits)->toHaveKey($orangeGlassKey);
// Verify variants are removed from product.variants
expect($product->variants)->not->toContain($kiwiVariant->id);
expect($product->variants)->not->toContain($kiwiGlassVariant->id);
// CHAPTER 7: The New Webshop Era
// After months of hard work, That One Drink launches their new webshop!
// The new platform has advanced deep-linking capabilities - customers can
// share direct links to specific variants (e.g., "Original in Glass").
// This means we no longer need separate product pages for Cans vs Glass!
//
// Time to simplify: We'll unsplit the Container option and merge back
// to having just one page per flavor. Each flavor page will show both
// the Can and Glass options within it.
verb(new VariantOptionWasUnsplit(
organization_id: $organization->id,
option_id: $containerOption->id,
));
$product->fresh();
$organization->fresh();
// Container should be removed from split layers
expect($product->split_layers)->toBe([$flavorOption->id]);
// We have 3 splits: Original, Orange, and Kiwi (empty, persisted)
expect($product->splits)->toHaveCount(3);
$simplifiedOriginalKey = $flavorOption->id . ':original';
$simplifiedOrangeKey = $flavorOption->id . ':orange';
$simplifiedKiwiKey = $flavorOption->id . ':kiwi';
expect($product->splits)->toHaveKey($simplifiedOriginalKey);
expect($product->splits)->toHaveKey($simplifiedOrangeKey);
expect($product->splits)->toHaveKey($simplifiedKiwiKey); // Empty but persisted
// Load the merged splits
$simplifiedOriginalSplit = ProductSplitState::load($product->splits[$simplifiedOriginalKey]);
$simplifiedOrangeSplit = ProductSplitState::load($product->splits[$simplifiedOrangeKey]);
// Original split should now contain both Can and Glass variants
expect($simplifiedOriginalSplit->name)->toBe('Original')
->and($simplifiedOriginalSplit->option_selections)->toBe([$flavorOption->id => 'original'])
->and($simplifiedOriginalSplit->path_key)->toBe($simplifiedOriginalKey);
// Orange split should now contain both Can and Glass variants
expect($simplifiedOrangeSplit->name)->toBe('Orange')
->and($simplifiedOrangeSplit->option_selections)->toBe([$flavorOption->id => 'orange'])
->and($simplifiedOrangeSplit->path_key)->toBe($simplifiedOrangeKey);
// Verify variant projections have correct split_id
expect(Variant::find($variant->id)->split_id)->toBe($simplifiedOriginalSplit->id);
expect(Variant::find($originalGlassVariant->id)->split_id)->toBe($simplifiedOriginalSplit->id);
expect(Variant::find($orangeCanVariant->id)->split_id)->toBe($simplifiedOrangeSplit->id);
expect(Variant::find($orangeGlassVariant->id)->split_id)->toBe($simplifiedOrangeSplit->id);
// The old container-level splits should be gone from the product
expect($product->splits)->not->toHaveKey($originalCanKey);
expect($product->splits)->not->toHaveKey($originalGlassKey);
expect($product->splits)->not->toHaveKey($orangeCanKey);
expect($product->splits)->not->toHaveKey($orangeGlassKey);
// Verify the container option is no longer marked for splitting
$containerOption->fresh();
expect($containerOption->is_split)->toBe(false);
// Success! That One Drink now has a simpler, more modern setup:
// - Two product pages: Original and Orange
// - Each page lets customers choose Can or Glass
// - Deep links work beautifully: /products/original?variant=glass
// - The business can pivot their strategy as their platform evolves
// EPILOGUE: Back to Basics
// After a year of analytics, the team has discovered something profound:
// customers prefer browsing ALL drinks together, using filters for flavor
// and container. The split pages were actually hurting discovery!
//
// The CEO makes the call: "Let's go back to one unified product page.
// We'll use variant selectors like we did in the beginning, but now
// we have the data to make it work perfectly."
//
// Time to unsplit Flavor as well and return to the default split.
verb(new VariantOptionWasUnsplit(
organization_id: $organization->id,
option_id: $flavorOption->id,
));
$product->fresh();
$organization->fresh();
// Both options should be removed from split layers
expect($product->split_layers)->toBe([]);
// We should now have exactly 1 split: the default split
expect($product->splits)->toHaveCount(1);
expect($product->splits)->toHaveKey('default');
// Load the default split - it should contain all 4 active variants
$finalDefaultSplit = ProductSplitState::load($product->splits['default']);
expect($finalDefaultSplit->name)->toBe('Default')
->and($finalDefaultSplit->is_default)->toBe(true) // Reused from first split transformation
->and($finalDefaultSplit->option_selections)->toBe([])
->and($finalDefaultSplit->path_key)->toBe('default');
// Verify all 4 variants point to the default split
expect(Variant::find($variant->id)->split_id)->toBe($finalDefaultSplit->id);
expect(Variant::find($originalGlassVariant->id)->split_id)->toBe($finalDefaultSplit->id);
expect(Variant::find($orangeCanVariant->id)->split_id)->toBe($finalDefaultSplit->id);
expect(Variant::find($orangeGlassVariant->id)->split_id)->toBe($finalDefaultSplit->id);
// Verify the split projections in the database
expect(ProductSplit::where('product_id', $productId)->count())->toBe(1);
$defaultSplitProjection = ProductSplit::where('product_id', $productId)->first();
expect($defaultSplitProjection)->not->toBeNull();
expect($defaultSplitProjection->path_key)->toBe('default');
expect($defaultSplitProjection->variants()->count())->toBe(4);
expect($defaultSplitProjection->name)->toBe('Default');
// Verify flavor option is no longer marked for splitting
$flavorOption->fresh();
expect($flavorOption->is_split)->toBe(false);
// The old flavor-level splits should be completely gone
expect($product->splits)->not->toHaveKey($simplifiedOriginalKey);
expect($product->splits)->not->toHaveKey($simplifiedOrangeKey);
// BONUS CHAPTER: The A/B Testing Pivot
// Plot twist! Before celebrating the unified approach, the marketing team
// has one more request: "We need to A/B test different landing pages for
// Cans vs Glass bottles. Let's split by Container instead of Flavor!"
//
// First, let's split by Flavor again (we just unsplit it)
verb(new VariantOptionWasSplit(
organization_id: $organization->id,
option_id: $flavorOption->id,
));
$product->fresh();
// We should have 2 splits: Original and Orange (by flavor)
expect($product->splits)->toHaveCount(2);
expect($product->splits)->toHaveKey($simplifiedOriginalKey);
expect($product->splits)->toHaveKey($simplifiedOrangeKey);
// Now split by Container as well
verb(new VariantOptionWasSplit(
organization_id: $organization->id,
option_id: $containerOption->id,
));
$product->fresh();
$organization->fresh();
// We should now have 4 splits (Original Can/Glass, Orange Can/Glass)
expect($product->splits)->toHaveCount(4);
// Verify all 4 splits exist with correct names
$originalCanKey = $flavorOption->id . ':original|' . $containerOption->id . ':can';
$originalGlassKey = $flavorOption->id . ':original|' . $containerOption->id . ':glass';
$orangeCanKey = $flavorOption->id . ':orange|' . $containerOption->id . ':can';
$orangeGlassKey = $flavorOption->id . ':orange|' . $containerOption->id . ':glass';
expect($product->splits)->toHaveKeys([$originalCanKey, $originalGlassKey, $orangeCanKey, $orangeGlassKey]);
$originalCanSplit = ProductSplitState::load($product->splits[$originalCanKey]);
$originalGlassSplit = ProductSplitState::load($product->splits[$originalGlassKey]);
$orangeCanSplit = ProductSplitState::load($product->splits[$orangeCanKey]);
$orangeGlassSplit = ProductSplitState::load($product->splits[$orangeGlassKey]);
expect($originalCanSplit->name)->toBe('Original / Can');
expect($originalGlassSplit->name)->toBe('Original / Glass');
expect($orangeCanSplit->name)->toBe('Orange / Can');
expect($orangeGlassSplit->name)->toBe('Orange / Glass');
// Now here's the pivot: unsplit by Flavor (the first option)
// This should give us 2 splits: Can and Glass
verb(new VariantOptionWasUnsplit(
organization_id: $organization->id,
option_id: $flavorOption->id,
));
$product->fresh();
$organization->fresh();
// Split layers should now only have Container
expect($product->split_layers)->toBe([$containerOption->id]);
// We should have 2 splits: Can and Glass
expect($product->splits)->toHaveCount(2);
$canKey = $containerOption->id . ':can';
$glassKey = $containerOption->id . ':glass';
expect($product->splits)->toHaveKey($canKey);
expect($product->splits)->toHaveKey($glassKey);
// Load the merged splits and verify their names
$canSplit = ProductSplitState::load($product->splits[$canKey]);
$glassSplit = ProductSplitState::load($product->splits[$glassKey]);
// THIS IS THE KEY TEST: Names should be just "Can" and "Glass", not "Original / Can"
expect($canSplit->name)->toBe('Can')
->and($canSplit->option_selections)->toBe([$containerOption->id => 'can'])
->and($canSplit->path_key)->toBe($canKey)
->and($canSplit->is_default)->toBe(false);
expect($glassSplit->name)->toBe('Glass')
->and($glassSplit->option_selections)->toBe([$containerOption->id => 'glass'])
->and($glassSplit->path_key)->toBe($glassKey)
->and($glassSplit->is_default)->toBe(false);
// Verify variant projections have correct split_id
expect(Variant::find($variant->id)->split_id)->toBe($canSplit->id);
expect(Variant::find($orangeCanVariant->id)->split_id)->toBe($canSplit->id);
expect(Variant::find($originalGlassVariant->id)->split_id)->toBe($glassSplit->id);
expect(Variant::find($orangeGlassVariant->id)->split_id)->toBe($glassSplit->id);
// The old 4 splits should be gone
expect($product->splits)->not->toHaveKey($originalCanKey);
expect($product->splits)->not->toHaveKey($originalGlassKey);
expect($product->splits)->not->toHaveKey($orangeCanKey);
expect($product->splits)->not->toHaveKey($orangeGlassKey);
// Perfect! Now we can A/B test landing pages for Can vs Glass products.
// Let's clean up and go back to default for the finale.
verb(new VariantOptionWasUnsplit(
organization_id: $organization->id,
option_id: $containerOption->id,
));
$product->fresh();
expect($product->splits)->toHaveCount(1);
expect($product->splits)->toHaveKey('default');
// INTERMISSION: Full Circle on the Drinks
// That One Drink is now back where they started, but with so much more:
// - 4 successful product variants (Original Can/Glass, Orange Can/Glass)
// - Deep understanding of their customers from the split experiment
// - A platform that can adapt as their business needs change
// - Confidence that they can try new strategies without technical debt
//
// The journey from one split → flavor splits → flavor+container splits
// → flavor splits → back to one split → flavor+container again
// → container splits → back to one split proves the system's flexibility.
// That One Drink can now focus on what matters: making great beverages! 🥤
// ═══════════════════════════════════════════════════════════════════════
// CHAPTER 8: The Merch Drop
// ═══════════════════════════════════════════════════════════════════════
//
// That One Drink has become a cultural phenomenon! Social media is flooded
// with fans showing off their drink of choice. The marketing team sees an
// opportunity: merchandise! They'll start simple - a black t-shirt with
// the iconic TOD logo.
//
// Unlike the drinks, this product starts with just one variant. No sizes
// yet (they're testing demand), no colors (black is classic), just
// "The TOD Tee" in all its glory.
$tshirt = verb(new ProductCreated(
organization_id: $organization->id,
handle: 'tod-tee',
name: 'The TOD Tee',
))->state(ProductState::class);
$tshirtDefaultVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $tshirt->id,
option_selections: [],
sku: 'TOD-TEE-001',
name: 'Default',
))->state(VariantState::class);
// Verify t-shirt has a default split with one variant
$tshirt->fresh();
expect($tshirt->splits)->toHaveCount(1)
->and($tshirt->splits)->toHaveKey('default');
$tshirtDefaultSplit = ProductSplitState::load($tshirt->splits['default']);
expect(Variant::find($tshirtDefaultVariant->id)->split_id)->toBe($tshirtDefaultSplit->id);
// ═══════════════════════════════════════════════════════════════════════
// CHAPTER 9: "I'm a Kiwi Person!"
// ═══════════════════════════════════════════════════════════════════════
//
// The TOD Tee sells out in 48 hours. The feedback is overwhelming, but
// one request keeps coming up: "Can I get a shirt that shows my flavor?"
//
// Fans have strong opinions. Original loyalists, Orange converts, and
// yes - even the Kiwi fans who are still mourning their discontinued
// favorite. The team realizes flavor identity is part of the brand now.
//
// Time to add flavor variants to the t-shirt! They'll reuse the existing
// Flavor option (why create a new one?) and split by it so each flavor
// gets its own product page with matching branding.
// First, assign the existing Flavor option to the t-shirt
verb(new VariantOptionAssigned(
product_id: $tshirt->id,
option_id: $flavorOption->id,
));
$tshirt->fresh();
expect($tshirt->available_options)->toContain($flavorOption->id);
// Now mark Flavor as a split option for the t-shirt
// This will subdivide the default split into flavor-specific splits
verb(new VariantOptionWasSplit(
organization_id: $organization->id,
option_id: $flavorOption->id,
));
$tshirt->fresh();
$flavorOption->fresh();
// The t-shirt should now have splits for Original and Orange
// (Kiwi was discontinued, so no variants exist for it yet)
// But wait - our default variant doesn't have a flavor selection yet!
// It should still be in a "limbo" state until we assign a flavor.
// Actually, since no variants have flavor selections yet, we need to
// update our default variant to specify which flavor it represents.
// Let's make it the Original flavor t-shirt.
verb(new VariantChangedOptionSelections(
organization_id: $organization->id,
variant_id: $tshirtDefaultVariant->id,
option_selections: [
$flavorOption->id => 'original',
],
));
// Refresh states
$tshirt->fresh();
$tshirtDefaultVariant->fresh();
// The variant should now have the flavor selection
expect($tshirtDefaultVariant->option_selections)->toBe([
$flavorOption->id => 'original',
]);
// The default split should have been TRANSFORMED into Original (ID preserved!)
$originalTshirtSplitKey = $flavorOption->id . ':original';
expect($tshirt->splits)->toHaveKey($originalTshirtSplitKey);
$originalTshirtSplit = ProductSplitState::load($tshirt->splits[$originalTshirtSplitKey]);
expect($originalTshirtSplit->name)->toBe('Original');
// CRITICAL: The split ID must be preserved when transforming from default to flavor-specific
// This is essential for external systems (Sync context) that reference splits by stable IDs
expect($originalTshirtSplit->id)->toBe($tshirtDefaultSplit->id);
// Verify the variant projection has the correct split_id
$tshirtVariantProjection = Variant::find($tshirtDefaultVariant->id);
expect($tshirtVariantProjection->split_id)->toBe($originalTshirtSplit->id);
// Now let's create the Orange flavor t-shirt!
$orangeTshirtVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $tshirt->id,
option_selections: [
$flavorOption->id => 'orange',
],
sku: 'TOD-TEE-002',
name: 'Orange Tee',
))->state(VariantState::class);
$tshirt->fresh();
// Orange split should now exist
$orangeTshirtSplitKey = $flavorOption->id . ':orange';
expect($tshirt->splits)->toHaveKey($orangeTshirtSplitKey);
$orangeTshirtSplit = ProductSplitState::load($tshirt->splits[$orangeTshirtSplitKey]);
expect($orangeTshirtSplit->name)->toBe('Orange');
expect(Variant::find($orangeTshirtVariant->id)->split_id)->toBe($orangeTshirtSplit->id);
// Verify the database projections
expect(ProductSplit::where('product_id', $tshirt->id)->count())->toBe(2);
expect(Variant::find($orangeTshirtVariant->id)->split_id)->toBe($orangeTshirtSplit->id);
// ═══════════════════════════════════════════════════════════════════════
// EPILOGUE: The Kiwi Comeback
// ═══════════════════════════════════════════════════════════════════════
//
// Plot twist! The Kiwi fans' persistence pays off. After months of
// "Bring Back Kiwi" campaigns, the team announces a limited edition
// Kiwi t-shirt - even though the drink is discontinued!
//
// "Kiwi Forever" becomes the rallying cry. The t-shirt sells out
// faster than any other product in TOD history.
$kiwiTshirtVariant = verb(new VariantCreated(
organization_id: $organization->id,
product_id: $tshirt->id,
option_selections: [
$flavorOption->id => 'kiwi',
],
sku: 'TOD-TEE-003',
name: 'Kiwi Forever Tee',
))->state(VariantState::class);
$tshirt->fresh();
// Kiwi split should now exist for the t-shirt
$kiwiTshirtSplitKey = $flavorOption->id . ':kiwi';
expect($tshirt->splits)->toHaveKey($kiwiTshirtSplitKey);
$kiwiTshirtSplit = ProductSplitState::load($tshirt->splits[$kiwiTshirtSplitKey]);
expect($kiwiTshirtSplit->name)->toBe('Kiwi');
expect(Variant::find($kiwiTshirtVariant->id)->split_id)->toBe($kiwiTshirtSplit->id);
// Final verification: T-shirt has 3 splits (Original, Orange, Kiwi)
expect($tshirt->splits)->toHaveCount(3);
expect(ProductSplit::where('product_id', $tshirt->id)->count())->toBe(3);
// THE END: From Beverage to Lifestyle Brand
//
// That One Drink started as a simple canned beverage. Now it's a
// lifestyle brand with:
// - 4 drink variants across 2 flavors and 2 containers
// - 3 t-shirt variants representing flavor identity
// - A flexible platform that lets them experiment with product structure
// - Proof that even discontinued products can live on in new forms
//
// The Kiwi fans finally got their win. 🥝👕
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment