diff --git a/src/StorefrontPlugin.php b/src/StorefrontPlugin.php index f51209b..d96ef11 100644 --- a/src/StorefrontPlugin.php +++ b/src/StorefrontPlugin.php @@ -60,6 +60,10 @@ public function register(PluginContext $context): void $context->routes()->post('shop', '/cart/update', static fn (Request $r, array $p): Response => $cart->update($r)); $context->routes()->post('shop', '/checkout', static fn (Request $r, array $p): Response => $cart->checkout($r)); + // Feed live featured products into the themed home page (ADR 0027) — a + // theme with a "featured" row renders them; others simply ignore the data. + $context->viewData()->register(new StorefrontViewData($port)); + // Teach an agent what the storefront is (ADR 0013). $context->skills()->register('Storefront', Guide::text()); } diff --git a/src/StorefrontViewData.php b/src/StorefrontViewData.php new file mode 100644 index 0000000..053a6bf --- /dev/null +++ b/src/StorefrontViewData.php @@ -0,0 +1,59 @@ +>}|array{} + */ + public function data(PageContext $page): array + { + // Only the landing page wants a featured row; skip the query elsewhere. + if ($page->kind !== 'home') { + return []; + } + $port = ($this->port)(); + if ($port === null) { + return []; + } + + // Featured-first, then keep only genuinely featured items (the sort floats + // them up but does not filter), capped to a handful. + $featured = []; + foreach ($port->list(['sort' => 'featured', 'page' => 1])['items'] as $item) { + if ($item['featured'] === true) { + $featured[] = $item; + if (count($featured) >= self::LIMIT) { + break; + } + } + } + + return $featured === [] ? [] : ['featured' => $featured]; + } +} diff --git a/tests/StorefrontViewDataTest.php b/tests/StorefrontViewDataTest.php new file mode 100644 index 0000000..6753ee0 --- /dev/null +++ b/tests/StorefrontViewDataTest.php @@ -0,0 +1,107 @@ + $port); + } + + public function test_it_contributes_only_featured_items_on_the_home_page(): void + { + $data = ($this->contributor(new FakeFeaturedPort()))->data($this->page('home')); + + self::assertArrayHasKey('featured', $data); + $skus = array_map(static fn (array $i): string => $i['sku_code'], $data['featured']); + self::assertSame(['a', 'c'], $skus, 'only featured=true items, in featured-first order'); + } + + public function test_it_caps_the_list_to_a_handful(): void + { + $data = ($this->contributor(new FakeFeaturedPort(featuredCount: 20)))->data($this->page('home')); + self::assertCount(6, $data['featured'], 'capped at the LIMIT'); + } + + public function test_it_is_silent_off_the_home_page(): void + { + self::assertSame([], ($this->contributor(new FakeFeaturedPort()))->data($this->page('entry'))); + self::assertSame([], ($this->contributor(new FakeFeaturedPort()))->data($this->page('collection'))); + } + + public function test_without_inventory_it_contributes_nothing(): void + { + self::assertSame([], ($this->contributor(null))->data($this->page('home'))); + } + + public function test_no_featured_items_contributes_nothing(): void + { + self::assertSame([], ($this->contributor(new FakeFeaturedPort(featuredCount: 0)))->data($this->page('home'))); + } +} + +/** A CatalogReadPort double: emits N featured items interleaved with non-featured. */ +final class FakeFeaturedPort implements CatalogReadPort +{ + public function __construct(private int $featuredCount = 2) + { + } + + public function list(array $filters): array + { + // Default shape: featured 'a', non-featured 'b', featured 'c' — proves + // filtering keeps only featured and preserves order. For other counts, + // emit `featuredCount` featured items (+ one non-featured decoy). + if ($this->featuredCount === 2) { + $items = [ + $this->item('a', true), + $this->item('b', false), + $this->item('c', true), + ]; + } else { + $items = [$this->item('decoy', false)]; + for ($i = 0; $i < $this->featuredCount; $i++) { + $items[] = $this->item('f' . $i, true); + } + } + return ['items' => $items, 'total' => count($items), 'page' => 1, 'per_page' => 24, 'pages' => 1]; + } + + public function get(string $sku): ?array + { + return null; + } + + public function categories(): array + { + return []; + } + + /** @return array */ + private function item(string $sku, bool $featured): array + { + return [ + 'sku_code' => $sku, 'name' => ucfirst($sku), 'price' => '1.00', 'unit' => null, + 'description' => null, 'image_media_id' => null, 'category_id' => null, + 'category' => null, 'featured' => $featured, 'availability' => 'in_stock', + ]; + } +}