# Product Page Conversion Upgrade - API/Data Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enrich the public item detail API so the web product page can display seller trust, shipping/returns, category breadcrumbs, and recommendation-ready data without extra client-side round trips or fragile hardcoded copy.

**Companion web plan:** `alqove-web/web/docs/superpowers/plans/2026-07-06-product-page-conversion-web-plan.md`.

**Primary endpoint:** `GET /v1/items/{item}` handled by `App\Modules\Items\Controllers\ItemController::showPublic()` and `App\Modules\Items\Resources\ItemResource`.

**Current state observed on 2026-07-06:**
- `ItemResource` returns core item fields, leaf category, store `id/name/city/state`, images, view/save counts, and timestamps.
- `Item::toSearchableArray()` already denormalizes store rating fields into Typesense documents.
- Store settings already include `flat_shipping_rate`, `free_shipping_threshold`, `minimum_order_amount`, `processing_days`, `return_window_days`, and `return_policy_text`.
- Store model already tracks review aggregates: `average_rating`, `review_count`, `avg_item_as_described`, `avg_shipping_speed`, `avg_communication`, and `avg_packaging`.
- The current public item detail response does not expose store settings, rating aggregates, verification status, or category ancestry.

**Implementation update on 2026-07-06:**
- Public item detail now returns `category_path` ordered root to leaf.
- Public item detail now returns nested `store.is_verified`, `store.is_suspended`, `store.average_rating`, `store.review_count`, `store.rating_breakdown`, and `store.policies`.
- `ItemController` eager-loads `store.settings`, `category.parent.parent`, and media for detail responses.
- `ItemResource` is null-safe for missing settings and no-review stores.
- `api/contracts/openapi.yaml` is updated and was synced into `alqove-web`; web generated types were rebuilt.
- Verified commands: `docker compose exec -T laravel.test php artisan test --filter=PublicItemDetailTest`. The API worker also ran `ItemCrudTest`, `ItemStatusTest`, `ItemImageTest`, `ItemSummaryResourceShapeTest`, and Pint for touched files.

**Implementation update after web recommendation rails:**
- No new recommendation endpoint is required for the current web implementation.
- The web product page uses existing `/v1/items` browse parameters for `More from this store` and `Similar items`.
- Keep the optional grouped recommendation endpoint deferred unless result quality, deduping, or overfetching becomes a real problem.

**Architecture:** Keep item detail data on `ItemResource` for v1. Add only fields needed by product-page presentation and SEO. Avoid creating a large bespoke view model unless `ItemResource` starts mixing seller-only and public-only concerns. If a dedicated public item resource is introduced later, keep response shape backward-compatible for web.

**Tech Stack:** Laravel 13, Sail, PostgreSQL, PHPUnit, Laravel Resources, OpenAPI contract snapshot, Laravel Scout/Typesense only for browse/search.

**Branch:** `feature/product-page-conversion-api` off `main`.

**Commands:**
```bash
docker compose exec -T laravel.test php artisan test --filter=Item
docker compose exec -T laravel.test php artisan test
docker compose exec -T laravel.test ./vendor/bin/pint
docker compose exec -T laravel.test ./vendor/bin/phpstan analyse
```

**Contract sync after API changes:**
```bash
# from alqove-web root, after updating alqove-api/api/contracts/openapi.yaml
./bin/sync-openapi.sh
npm run build:types
```

---

## File structure overview

**Likely modify:**
```
app/Modules/Items/Resources/ItemResource.php
app/Modules/Items/Controllers/ItemController.php
app/Models/Item.php
app/Models/Category.php
contracts/openapi.yaml
tests/Feature/Items/*
```

**Potentially create:**
```
app/Modules/Items/Resources/PublicItemResource.php
app/Modules/Items/Resources/StoreTrustResource.php
app/Modules/Items/Resources/StorePolicyResource.php
tests/Feature/Items/PublicItemDetailTest.php
tests/Unit/Items/CategoryPathTest.php
```

---

## Target response shape

Keep existing fields and add optional/nested data. Suggested shape:

```json
{
  "data": {
    "id": "uuid",
    "title": "Item title",
    "description": "...",
    "price": 4591,
    "original_retail": 12000,
    "condition": "EUC",
    "brand": "Zara",
    "size": "M",
    "colors": ["Black"],
    "measurements": { "chest": "19in" },
    "category": {
      "id": 18,
      "name": "Mini Dresses",
      "slug": "clothing-dresses-mini-dresses"
    },
    "category_path": [
      { "id": 1, "name": "Clothing", "slug": "clothing" },
      { "id": 14, "name": "Dresses", "slug": "clothing-dresses" },
      { "id": 18, "name": "Mini Dresses", "slug": "clothing-dresses-mini-dresses" }
    ],
    "store": {
      "id": "uuid",
      "name": "ReNew Fashion",
      "city": "Brooklyn",
      "state": "NY",
      "is_verified": true,
      "is_suspended": false,
      "average_rating": 4.8,
      "review_count": 37,
      "rating_breakdown": {
        "item_as_described": 4.9,
        "shipping_speed": 4.7,
        "communication": 4.8,
        "packaging": 4.8
      },
      "policies": {
        "flat_shipping_rate": 899,
        "free_shipping_threshold": 10000,
        "minimum_order_amount": null,
        "processing_days": 2,
        "return_window_days": 14,
        "return_policy_text": "..."
      }
    }
  }
}
```

Naming can be adjusted to match current API conventions, but the web plan should not need multiple extra API calls for these facts.

---

## Task 1 - Add category ancestry for breadcrumbs

**Goal:** Let the product page render `Home / Clothing / Dresses / Mini Dresses` without guessing.

- [x] **Step 1 (test):** Add a unit or feature test that creates a three-level category tree and asserts the public item detail response includes `category_path` ordered root to leaf.
- [x] **Step 2:** Add a helper on `Category` or reuse `Item::buildCategoryPath()` logic, but return full objects `{id,name,slug}` instead of only slugs.
- [x] **Step 3:** Ensure `showPublic()` eager-loads enough parent relationships to avoid N+1 queries. Existing `Item::buildCategoryPath()` loads `category.parent.parent`; that is likely enough for current three-level taxonomy.
- [x] **Step 4:** Add `category_path` to `ItemResource`.
- [x] **Step 5:** Update `contracts/openapi.yaml`.

**Acceptance criteria:**
- Public item detail includes full category ancestry.
- Missing/edge category data does not 500.

---

## Task 2 - Expose store trust fields on item detail

**Goal:** Let the web product page show seller credibility without an additional client-side rating-summary request.

- [x] **Step 1 (test):** Create a store with review aggregate fields and assert public item detail includes:
  - `is_verified`
  - `is_suspended`
  - `average_rating`
  - `review_count`
  - `rating_breakdown.item_as_described`
  - `rating_breakdown.shipping_speed`
  - `rating_breakdown.communication`
  - `rating_breakdown.packaging`
- [x] **Step 2:** Add those fields under the existing `store` object in `ItemResource`.
- [x] **Step 3:** Decide null handling:
  - average fields may be `null` when the store has no reviews
  - `review_count` should be integer, default 0
- [x] **Step 4:** Keep the existing top-level `store_average_rating` style fields out of the item detail response unless web already consumes them. Prefer nested `store` fields for detail page readability.
- [x] **Step 5:** Update OpenAPI and sync to `alqove-web`.

**Acceptance criteria:**
- Web can remove or avoid `useStoreRatingSummary` on item detail.
- No-review stores render predictably with count 0 and null averages.

---

## Task 3 - Expose store shipping and return policies

**Goal:** Let the product page show shipping cost, processing time, free shipping threshold, and returns/refund policy.

- [x] **Step 1 (test):** Create store settings and assert public item detail includes `store.policies` with:
  - `flat_shipping_rate`
  - `free_shipping_threshold`
  - `minimum_order_amount`
  - `processing_days`
  - `return_window_days`
  - `return_policy_text`
- [x] **Step 2:** Eager-load `store.settings` in `showPublic()`.
- [x] **Step 3:** Add `policies` under the `store` object in `ItemResource`. If settings are missing, return safe defaults or `null` values without failing.
- [x] **Step 4:** Confirm money values remain integer cents.
- [x] **Step 5:** Update OpenAPI and sync to `alqove-web`.

**Acceptance criteria:**
- Product page can render accurate policy text.
- Missing settings do not break item detail.

---

## Task 4 - Add public item recommendation support if needed

**Goal:** Support "More from this store" and "Similar items" with stable, efficient data. Start with existing browse/search; add endpoints only if frontend composition becomes too awkward.

- [x] **Step 1:** Confirm the web plan can implement "More from this store" using existing `/v1/items?store_id[]=...` and "Similar items" using `category_slug`, `brand`, `size`, and price filters.
- [ ] **Step 2 (optional):** If browse params are insufficient, add `GET /v1/items/{item}/recommendations` returning grouped rails:
  - `more_from_store`
  - `similar_items`
- [ ] **Step 3 (optional test):** Assert current item is excluded, only active/verified-store items are returned, and each rail has a reasonable limit.
- [ ] **Step 4 (optional):** Keep recommendation response shape compatible with `ItemBrowseResource`/`ItemSummaryData`.
- [ ] **Step 5 (optional):** Update OpenAPI and sync to `alqove-web`.

**Acceptance criteria:**
- Frontend can render recommendation rails without overfetching or duplicating fragile query logic.
- No new endpoint is added unless it clearly improves maintainability.

---

## Task 5 - Improve local seed/demo data for product pages

**Goal:** Make local product pages useful for visual QA by ensuring seeded active items have realistic fields and at least some images.

- [x] **Step 1:** Review `database/seeders/ItemSeeder.php` and current factories.
- [x] **Step 2:** Ensure active seeded items set `published_at` if product sorting or future UI depends on it. Current Typesense fallback uses `created_at`, but explicit `published_at` is clearer. _(Set in `ItemSeeder`, not `ItemFactory` — seven test files assert on `published_at`, so the factory default is deliberately left alone. Drafts stay null; sold items publish before they sell.)_
- [x] **Step 3:** Add richer measurements to a subset of seeded items. _(`ItemSeeder::measurements()` picks a tops/bottoms/dresses profile; ~35% stay null to exercise the empty state.)_
- [x] **Step 4:** Add a small deterministic local media fixture set, if acceptable for the repo. Avoid remote image fetches in seeders. _(No binaries committed: `DemoMediaSeeder` draws placeholders with GD at seed time. Palette, slot count, and motif derive from `crc32(item->id)`, so a given DB always renders the same gallery. Degrades gracefully — skips if `gd` is missing, drops labels if no TTF font is present.)_
- [x] **Step 5:** Attach images to a subset of active items through MediaLibrary so the web gallery can be tested. _(1-3 photos per active item into the `images` collection; 154 images across 87 items locally, all four conversions generated.)_
- [x] **Step 6:** Keep seeders idempotent if they may be run repeatedly in dev. Existing seeders create duplicate data, so do not worsen that behavior; prefer a dedicated demo media seeder if needed. _(Dedicated `DemoMediaSeeder`; items that already have images are skipped — a re-run reported "attached 0, skipped 87".)_

**Acceptance criteria:**
- Local product pages no longer all show "No image."
- Demo data is deterministic enough for screenshots.
- Seeders do not require network access.

**Implementation update on 2026-07-16:**
- `DemoMediaSeeder` added and registered in `DatabaseSeeder` after `ItemSeeder`.
- No test depends on `DatabaseSeeder`/`ItemSeeder` (tests only seed `RoleAndPermissionSeeder`), so seeding changes do not touch the suite.
- Verified end to end: `GET /v1/items/{item}` returns image URLs plus `category_path` ("Clothing / Bottoms / Skirts"), store trust, and policies; the web item page returns 200. Runtime ~2 min for 154 images.
- Still open: browser/Playwright visual QA of the gallery (web plan Task 8).

---

## Task 6 - Update OpenAPI and generated web types

**Goal:** Keep the API contract and web TypeScript types aligned.

- [x] **Step 1:** Update `api/contracts/openapi.yaml` for all new item detail fields.
- [x] **Step 2:** From `alqove-web`, sync the API contract. _(PowerShell `Copy-Item` was used because the Bash helper did not resolve the Windows path.)_
- [x] **Step 3:** Run `npm run build:types`.
- [x] **Step 4:** Update manual types in `packages/types/src/index.ts` only if generated types do not cover local convenience interfaces.
- [x] **Step 5:** Run web typecheck.

**Acceptance criteria:**
- Contract includes the enriched item detail shape.
- `@alqove/types` exposes the fields used by the web plan.

---

## Task 7 - Tests and verification

**Goal:** Ensure item detail remains public, safe, and performant.

- [x] **Step 1:** Add/extend feature tests for `GET /v1/items/{item}`:
  - active item from verified, non-suspended store returns 200
  - draft/sold/removed item returns 404
  - item from suspended store returns 404
  - item from unverified store returns 404
  - response includes category path, store trust, and policies
- [x] **Step 2:** Run targeted tests on Postgres through Sail.
- [x] **Step 3:** Run full API tests if practical. _(2026-07-16: 1530 passed, 10 skipped, 4726 assertions.)_
- [x] **Step 4:** Run Pint.
- [x] **Step 5:** Run PHPStan and note any pre-existing unrelated errors. _(2026-07-16: 871 errors, none in the files touched here. The repo runs phpstan at level 6 with no baseline file, so that count is a standing, tolerated total of pre-existing errors — mostly `missingType.iterableValue` and model `property.notFound` — and CI does not gate on it.)_
- [x] **Step 6:** Re-import Typesense if changes affect browse/search documents:
  `docker compose exec -T laravel.test env SCOUT_QUEUE=false php artisan scout:import "App\\Models\\Item" --no-interaction`
  _(Not needed for the ItemResource enrichment — item detail reads the database. It IS needed once Task 5 attaches media: `toSearchableArray()` carries `thumbnail_url`, so items indexed before their photos existed keep rendering "No image" in browse grids and the product page's recommendation rails, even though the gallery is fine. Caught during visual QA on 2026-07-16 and re-imported. `bin/setup.sh` already runs `scout:import` after `migrate:fresh --seed`, so a fresh bootstrap is correct; `DemoMediaSeeder` also refreshes the documents it touches, best-effort, to keep standalone re-runs honest.)_

**Acceptance criteria:**
- Public item detail is richer without exposing private seller/admin data.
- Existing buyer browse and seller item endpoints are not regressed.

---

## Suggested implementation order

1. Task 1 - Category ancestry.
2. Task 2 - Store trust fields.
3. Task 3 - Store policies.
4. Task 6 - OpenAPI and web types.
5. Task 5 - Demo media/seed data.
6. Task 4 - Recommendation endpoint only if needed.
7. Task 7 - Full verification.

This order unlocks the frontend product-page upgrade quickly while keeping optional recommendation infrastructure separate.
