/bin/zsh
-lc
sed -n '24,44p' specs/progress.md

## Acceptance Checklist

| Area | Criteria Source | Status | Evidence |
| --- | --- | --- | --- |
| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. OAuth/app API endpoints and broader admin REST surfaces are still missing. |
| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms, and customer order list/detail views render seeded and runtime data. |
| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
| Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 17 navigation items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, and no product media/runtime carts. Order/payment seed data is still missing. |
| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin navigation item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final verification and completion audit are next. |

## Verification Evidence

- 2026-05-03: `mcp__laravel_boost__.application_info` confirmed PHP 8.4, Laravel 12.51.0, Livewire 4.1.4, Flux 2.12.0, Pest 4.3.2, SQLite.
- 2026-05-03: `mcp__laravel_boost__.database_schema(summary: true)` returned no tables for the current configured database.
- 2026-05-03: `git status --short` showed no current worktree changes before implementation.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Laravel 12 middleware/auth/authorization/migration docs plus Livewire 4 and Pest 4 docs before code changes.


/bin/zsh
-lc
tail -n 95 specs/progress.md

- 2026-05-04: `npm run build` passed after the checkout/notification settings Blade changes.
- 2026-05-04: `php artisan test --compact` passed after the checkout/notification settings changes: 198 tests, 999 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the checkout/notification settings changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url` resolved `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` render with the full settings tab set; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 nested array form docs, Laravel 12 relationship sync/validation docs, Flux UI controls, and Pest 4 expectations before the product option matrix changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the product option matrix changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Catalog/CatalogUiTest.php` passed after the product option matrix changes and route smoke stabilization: 5 tests, 55 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Catalog tests/Feature/Admin/ProductMediaManagementTest.php` passed after the product option matrix changes: 28 tests, 150 assertions.
- 2026-05-04: `npm run build` passed after the product option matrix Blade change.
- 2026-05-04: `php artisan test --compact` passed after the product option matrix changes: 199 tests, 1010 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the product option matrix changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/products/create")` resolved `http://shop.test/admin/products/create` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/products/create` renders the product form after the matrix control changes; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 textarea/form/testing docs, Flux UI textarea/form controls, Laravel 12 JSON settings and route model binding docs, and Pest 4 docs before the theme file editor changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the theme file editor changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin/ContentManagementTest.php` passed after the theme file editor changes: 6 tests, 47 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin` passed after the theme file editor changes: 44 tests, 280 assertions.
- 2026-05-04: `npm run build` passed after the theme editor Blade changes.
- 2026-05-04: `php artisan test --compact` passed after the theme file editor changes: 200 tests, 1019 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the theme file editor changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/themes/1/editor")` resolved `http://shop.test/admin/themes/1/editor` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/themes/1/editor` renders the theme file list and switches to the `sections/hero.blade.php` file editor textarea; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.

## Decisions

- Follow the roadmap order strictly. Phase 1 is required before catalog, checkout, admin, storefront, and QA flows can be meaningfully implemented.
- Preserve the starter Fortify conventions where they do not conflict with the shop specs.
- Use integer minor units for all money columns and JSON text columns cast through Eloquent as specified.
- Keep `password_hash` as the database column for admin users and customers while exposing `password`/`getAuthPassword()` at the model layer for Fortify/Laravel auth compatibility.
- Seed both `shop.test` and `acme-fashion.test` as storefront domains for the first store so the goal URL and E2E spec URL can both resolve.
- Customer Livewire auth components persist `storeId` from the initial storefront request because Livewire update requests do not run through the original storefront route middleware.
- The seed specification contains an arithmetic conflict: Acme Fashion's per-product variant table sums to 117 variants, while the prose says 107. The implementation follows the concrete per-product table, resulting in 117 Fashion variants and 10 Electronics variants, 127 total.
- Child catalog models without a `store_id` column are tenant-scoped th…6419 chars truncated…to sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store, but it is not Sanctum/Passport authentication yet.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.

## Open Issues

- Composer does not currently include Sanctum or Passport, while Spec 06 requires Sanctum personal access tokens and Spec 02 defers Passport OAuth. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Navigation drag-and-drop nesting is still missing; the current admin supports flat ordered menu items with up/down controls.
- OAuth/Passport app API endpoints and broader admin REST endpoints outside order management are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, order API surfaces, and outbound webhook delivery foundations are implemented, with known auth/token, navigation nesting, OAuth/app API, browser-test, and database CHECK constraint gaps tracked above.


Output:
Chunk ID: 61b2c2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2279
Output:

## Acceptance Checklist

| Area | Criteria Source | Status | Evidence |
| --- | --- | --- | --- |
| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. OAuth/app API endpoints and broader admin REST surfaces are still missing. |
| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms, and customer order list/detail views render seeded and runtime data. |
| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
| Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 17 navigation items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, and no product media/runtime carts. Order/payment seed data is still missing. |
| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin navigation item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final verification and completion audit are next. |

## Verification Evidence

- 2026-05-03: `mcp__laravel_boost__.application_info` confirmed PHP 8.4, Laravel 12.51.0, Livewire 4.1.4, Flux 2.12.0, Pest 4.3.2, SQLite.
- 2026-05-03: `mcp__laravel_boost__.database_schema(summary: true)` returned no tables for the current configured database.
- 2026-05-03: `git status --short` showed no current worktree changes before implementation.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Laravel 12 middleware/auth/authorization/migration docs plus Livewire 4 and Pest 4 docs before code changes.


Output:
Chunk ID: 1ad351
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4105
Output:
- 2026-05-04: `npm run build` passed after the checkout/notification settings Blade changes.
- 2026-05-04: `php artisan test --compact` passed after the checkout/notification settings changes: 198 tests, 999 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the checkout/notification settings changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url` resolved `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` render with the full settings tab set; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 nested array form docs, Laravel 12 relationship sync/validation docs, Flux UI controls, and Pest 4 expectations before the product option matrix changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the product option matrix changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Catalog/CatalogUiTest.php` passed after the product option matrix changes and route smoke stabilization: 5 tests, 55 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Catalog tests/Feature/Admin/ProductMediaManagementTest.php` passed after the product option matrix changes: 28 tests, 150 assertions.
- 2026-05-04: `npm run build` passed after the product option matrix Blade change.
- 2026-05-04: `php artisan test --compact` passed after the product option matrix changes: 199 tests, 1010 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the product option matrix changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/products/create")` resolved `http://shop.test/admin/products/create` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/products/create` renders the product form after the matrix control changes; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 textarea/form/testing docs, Flux UI textarea/form controls, Laravel 12 JSON settings and route model binding docs, and Pest 4 docs before the theme file editor changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the theme file editor changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin/ContentManagementTest.php` passed after the theme file editor changes: 6 tests, 47 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin` passed after the theme file editor changes: 44 tests, 280 assertions.
- 2026-05-04: `npm run build` passed after the theme editor Blade changes.
- 2026-05-04: `php artisan test --compact` passed after the theme file editor changes: 200 tests, 1019 assertions.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the theme file editor changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/themes/1/editor")` resolved `http://shop.test/admin/themes/1/editor` before browser verification.
- 2026-05-04: Playwright MCP verified `http://shop.test/admin/themes/1/editor` renders the theme file list and switches to the `sections/hero.blade.php` file editor textarea; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.

## Decisions

- Follow the roadmap order strictly. Phase 1 is required before catalog, checkout, admin, storefront, and QA flows can be meaningfully implemented.
- Preserve the starter Fortify conventions where they do not conflict with the shop specs.
- Use integer minor units for all money columns and JSON text columns cast through Eloquent as specified.
- Keep `password_hash` as the database column for admin users and customers while exposing `password`/`getAuthPassword()` at the model layer for Fortify/Laravel auth compatibility.
- Seed both `shop.test` and `acme-fashion.test` as storefront domains for the first store so the goal URL and E2E spec URL can both resolve.
- Customer Livewire auth components persist `storeId` from the initial storefront request because Livewire update requests do not run through the original storefront route middleware.
- The seed specification contains an arithmetic conflict: Acme Fashion's per-product variant table sums to 117 variants, while the prose says 107. The implementation follows the concrete per-product table, resulting in 117 Fashion variants and 10 Electronics variants, 127 total.
- Child catalog models without a `store_id` column are tenant-scoped through their parent product relationship. Inventory keeps its denormalized `store_id` and enforces that it matches the variant product store at save time.
- Storefront content pages now resolve from the `pages` table and only published pages are rendered.
- The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
- The product admin form generates variant rows from the option cartesian product, preserves matching variant edits by option combination, and syncs product option values plus variant pivots on save; stale variants are deleted or archived if order-line history exists.
- Navigation tree support is flat for now because the Phase 3 schema defines `navigation_items.position` but no `parent_id`; `NavigationService::buildTree()` returns a flat tree-compatible array with empty children.
- Cart, checkout, and pricing runtime records are not seeded; only deterministic tax, shipping, and discount configuration is seeded. Runtime carts/checkouts are created by services and tests.
- The cart drawer reads the current session/customer cart without creating an empty cart on every storefront render; carts are created on first add-to-cart or checkout/cart service mutation.
- The checkout UI reserves inventory by selecting a payment method, then submits payment through `CheckoutService::completeCheckout()` so failed card payments can release reservations and return customers to payment selection.
- Order confirmation access is allowed for the authenticated owning customer or for the session that just placed the order via `last_order_id`.
- Customer account order list/detail routes use the `customer` guard and reload orders through explicit store/customer constraints.
- The cart REST API exposes `cart_version` as the public optimistic concurrency field while the service layer keeps its `expectedVersion` argument; `expected_version` remains accepted as a compatibility alias in API requests.
- Cart page discount codes are validated with `DiscountService`, saved in session, and applied to the checkout when the customer proceeds.
- Admin discount forms persist minimum purchase, product eligibility, collection eligibility, and `one_per_customer` in the existing `discounts.rules_json` keys consumed by `DiscountService`; code discounts now check customer order-line allocation history before allowing another use.
- `PricingEngine` applies an explicit checkout discount code first, then active automatic discounts returned by `DiscountService`.
- Order creation replays the same code-then-automatic discount sequence in memory so `order_lines.discount_allocations_json` keeps separate `{discount_id, code, amount}` entries for every line-item discount while preserving aggregate cart line totals.
- `orders.checkout_id` is intentionally added beyond the original schema table to enforce idempotent checkout completion without duplicate orders.
- Failed card payments release reserved inventory and move the checkout back to `shipping_selected` so customers can retry payment selection.
- Bank transfer order completion creates a pending order and payment while keeping inventory reserved until the later admin payment-confirmation flow.
- Inventory commit/release/restock locks by primary key without the current-store global scope because scheduled jobs can operate across stores in one run.
- Admin order Livewire components persist locked `storeId`/`orderId` state and reload orders without global scopes but with explicit store constraints before each service-backed action.
- Admin order actions deliberately delegate to `OrderService`, `RefundService`, and `FulfillmentService` so UI validation cannot bypass payment, refund, fulfillment, or inventory guards.
- Storefront order lookup uses a deterministic HMAC access token derived from store/order identity so confirmation links can be token-gated without adding a new runtime column.
- Admin order API routes currently use the existing session `auth` middleware and store-user membership checks because Sanctum is not installed yet.
- Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; the dedicated analytics page now reads analytics event and daily aggregate tables.
- Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.
- Admin/store Livewire route middleware is registered with Livewire persistent middleware so component POST requests keep `current_store` binding and store-role checks after the initial full-page route load.
- Admin routes use a custom `EnsureUserEmailIsVerified` middleware so verified-user gating works for full-page Livewire routes without relying on the stock redirect middleware path that fails during Livewire component redirects in this app.
- Admin settings are split across `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, and `/admin/settings/notifications`; domains are managed on the general settings page because the current route surface does not need a separate domains route.
- Checkout and notification settings persist in `store_settings.settings_json`; `bank_transfer_cancel_days` remains a root-level key because the existing cancellation job already consumes it from that location.
- Admin navigation persists flat ordered menu items with up/down controls because the schema has `position` but no parent/child column for nested drag-and-drop.
- The theme editor uses the `ThemeSettingsService` default settings shape as the editable schema and writes back to `theme_settings.settings_json`.
- Theme files are seeded and edited through local disk storage at `theme_files.storage_key`; saving a file updates the stored contents plus `sha256` and `byte_size` metadata.
- Theme duplication copies each file's local disk contents to a new storage key for the copied theme instead of sharing the original file storage path.
- Search uses a local SQLite FTS5 virtual table (`products_fts`) with `SearchService` as the only query/sync boundary; product observer events keep the index synchronized for create/update/delete.
- Search settings apply at query time: per-store stop words are removed, synonym groups expand into sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store, but it is not Sanctum/Passport authentication yet.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.

## Open Issues

- Composer does not currently include Sanctum or Passport, while Spec 06 requires Sanctum personal access tokens and Spec 02 defers Passport OAuth. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Navigation drag-and-drop nesting is still missing; the current admin supports flat ordered menu items with up/down controls.
- OAuth/Passport app API endpoints and broader admin REST endpoints outside order management are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, order API surfaces, and outbound webhook delivery foundations are implemented, with known auth/token, navigation nesting, OAuth/app API, browser-test, and database CHECK constraint gaps tracked above.


 M app/Policies/CollectionPolicy.php
 M app/Policies/CustomerPolicy.php
 M app/Policies/DiscountPolicy.php
 M app/Policies/FulfillmentPolicy.php
 M app/Policies/OrderPolicy.php
 M app/Policies/PagePolicy.php
 M app/Policies/ProductPolicy.php
 M app/Policies/RefundPolicy.php
 M app/Policies/ThemePolicy.php
 M tests/Feature/Foundation/AuthorizationTest.php


Output:
Chunk ID: db29eb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 90
Output:
 M app/Policies/CollectionPolicy.php
 M app/Policies/CustomerPolicy.php
 M app/Policies/DiscountPolicy.php
 M app/Policies/FulfillmentPolicy.php
 M app/Policies/OrderPolicy.php
 M app/Policies/PagePolicy.php
 M app/Policies/ProductPolicy.php
 M app/Policies/RefundPolicy.php
 M app/Policies/ThemePolicy.php
 M tests/Feature/Foundation/AuthorizationTest.php


The full suite is green after the policy refactor: 201 tests, 1075 assertions. I’m recording the evidence and removing the stale policy-typing open issue before committing.


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/specs/progress.md
@@
-| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
+| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
@@
 - 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the theme file editor changes.
 - 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/themes/1/editor")` resolved `http://shop.test/admin/themes/1/editor` before browser verification.
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin/themes/1/editor` renders the theme file list and switches to the `sections/hero.blade.php` file editor textarea; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 policy authorization docs and Pest 4 architecture/testing docs before the policy typing refactor.
+- 2026-05-04: `rg "object \\$" app/Policies tests/Feature/Foundation/AuthorizationTest.php` found no remaining generic policy model parameters after the policy typing refactor.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the policy typing refactor.
+- 2026-05-04: `php artisan test --compact tests/Feature/Foundation/AuthorizationTest.php` passed after the policy typing refactor: 5 tests, 68 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Foundation` passed after the policy typing refactor: 13 tests, 102 assertions.
+- 2026-05-04: `php artisan test --compact` passed after the policy typing refactor: 201 tests, 1075 assertions.
@@
 - Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
+- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.
@@
-- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
 - `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/specs/progress.md

@@ -33,3 +33,3 @@
 | Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
-| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
+| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, and one-time display developer token generation are implemented. Sanctum token auth is still missing. |
 | Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 17 navigation items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, and no product media/runtime carts. Order/payment seed data is still missing. |
@@ -293,2 +293,8 @@
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin/themes/1/editor` renders the theme file list and switches to the `sections/hero.blade.php` file editor textarea; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 policy authorization docs and Pest 4 architecture/testing docs before the policy typing refactor.
+- 2026-05-04: `rg "object \\$" app/Policies tests/Feature/Foundation/AuthorizationTest.php` found no remaining generic policy model parameters after the policy typing refactor.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the policy typing refactor.
+- 2026-05-04: `php artisan test --compact tests/Feature/Foundation/AuthorizationTest.php` passed after the policy typing refactor: 5 tests, 68 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Foundation` passed after the policy typing refactor: 13 tests, 102 assertions.
+- 2026-05-04: `php artisan test --compact` passed after the policy typing refactor: 201 tests, 1075 assertions.

@@ -349,2 +355,3 @@
 - Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
+- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.

@@ -355,3 +362,2 @@
 - Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
-- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
 - `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


 app/Policies/CollectionPolicy.php              |  7 +-
 app/Policies/CustomerPolicy.php                |  5 +-
 app/Policies/DiscountPolicy.php                |  7 +-
 app/Policies/FulfillmentPolicy.php             |  5 +-
 app/Policies/OrderPolicy.php                   | 11 ++--
 app/Policies/PagePolicy.php                    |  7 +-
 app/Policies/ProductPolicy.php                 | 11 ++--
 app/Policies/RefundPolicy.php                  |  3 +-
 app/Policies/ThemePolicy.php                   |  9 +--
 specs/progress.md                              | 10 ++-
 tests/Feature/Foundation/AuthorizationTest.php | 90 +++++++++++++++++++++++---
 11 files changed, 125 insertions(+), 40 deletions(-)


 M app/Policies/CollectionPolicy.php
 M app/Policies/CustomerPolicy.php
 M app/Policies/DiscountPolicy.php
 M app/Policies/FulfillmentPolicy.php
 M app/Policies/OrderPolicy.php
 M app/Policies/PagePolicy.php
 M app/Policies/ProductPolicy.php
 M app/Policies/RefundPolicy.php
 M app/Policies/ThemePolicy.php
 M specs/progress.md
 M tests/Feature/Foundation/AuthorizationTest.php


Output:
Chunk ID: d0ec54
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Output:
Chunk ID: f8a27f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 95
Output:
 M app/Policies/CollectionPolicy.php
 M app/Policies/CustomerPolicy.php
 M app/Policies/DiscountPolicy.php
 M app/Policies/FulfillmentPolicy.php
 M app/Policies/OrderPolicy.php
 M app/Policies/PagePolicy.php
 M app/Policies/ProductPolicy.php
 M app/Policies/RefundPolicy.php
 M app/Policies/ThemePolicy.php
 M specs/progress.md
 M tests/Feature/Foundation/AuthorizationTest.php


Output:
Chunk ID: 47f451
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 175
Output:
 app/Policies/CollectionPolicy.php              |  7 +-
 app/Policies/CustomerPolicy.php                |  5 +-
 app/Policies/DiscountPolicy.php                |  7 +-
 app/Policies/FulfillmentPolicy.php             |  5 +-
 app/Policies/OrderPolicy.php                   | 11 ++--
 app/Policies/PagePolicy.php                    |  7 +-
 app/Policies/ProductPolicy.php                 | 11 ++--
 app/Policies/RefundPolicy.php                  |  3 +-
 app/Policies/ThemePolicy.php                   |  9 +--
 specs/progress.md                              | 10 ++-
 tests/Feature/Foundation/AuthorizationTest.php | 90 +++++++++++++++++++++++---
 11 files changed, 125 insertions(+), 40 deletions(-)


git add app/Policies/CollectionPolicy.php app/Policies/CustomerPolicy.php app/Policies/DiscountPolicy.php app/Policies/FulfillmentPolicy.php app/Policies/OrderPolicy.php app/Policies/PagePolicy.php app/Policies/ProductPolicy.php app/Policies/RefundPolicy.php app/Policies/ThemePolicy.php tests/Feature/Foundation/AuthorizationTest.php specs/progress.md && git commit -m "Type resource policy models"


git add app/Policies/CollectionPolicy.php app/Policies/CustomerPolicy.php app/Policies/DiscountPolicy.php app/Policies/FulfillmentPolicy.php app/Policies/OrderPolicy.php app/Policies/PagePolicy.php app/Policies/ProductPolicy.php app/Policies/RefundPolicy.php app/Policies/ThemePolicy.php tests/Feature/Foundation/AuthorizationTest.php specs/progress.md && git commit -m "Type resource policy models"

[2026-05-04-codex-5-5-goal e8b6e3b5] Type resource policy models
 11 files changed, 125 insertions(+), 40 deletions(-)


Output:
Chunk ID: 479169
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 30
Output:
[2026-05-04-codex-5-5-goal e8b6e3b5] Type resource policy models
 11 files changed, 125 insertions(+), 40 deletions(-)


Committed `e8b6e3b5 Type resource policy models`. I’m checking the remaining gaps and moving to the next contained implementation slice.


git status --short && tail -n 28 specs/progress.md


git status --short && tail -n 28 specs/progress.md

- Search settings apply at query time: per-store stop words are removed, synonym groups expand into sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store, but it is not Sanctum/Passport authentication yet.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.

## Open Issues

- Composer does not currently include Sanctum or Passport, while Spec 06 requires Sanctum personal access tokens and Spec 02 defers Passport OAuth. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Navigation drag-and-drop nesting is still missing; the current admin supports flat ordered menu items with up/down controls.
- OAuth/Passport app API endpoints and broader admin REST endpoints outside order management are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, order API surfaces, and outbound webhook delivery foundations are implemented, with known auth/token, navigation nesting, OAuth/app API, browser-test, and database CHECK constraint gaps tracked above.


Output:
Chunk ID: 3e2b1d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1308
Output:
- Search settings apply at query time: per-store stop words are removed, synonym groups expand into sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store, but it is not Sanctum/Passport authentication yet.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.

## Open Issues

- Composer does not currently include Sanctum or Passport, while Spec 06 requires Sanctum personal access tokens and Spec 02 defers Passport OAuth. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Navigation drag-and-drop nesting is still missing; the current admin supports flat ordered menu items with up/down controls.
- OAuth/Passport app API endpoints and broader admin REST endpoints outside order management are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, order API surfaces, and outbound webhook delivery foundations are implemented, with known auth/token, navigation nesting, OAuth/app API, browser-test, and database CHECK constraint gaps tracked above.


The cleanest remaining dependency-free security slice is to make the existing `oauth_tokens` developer tokens usable for admin API authentication. That gives the app a documented compatible token path without adding Sanctum or Passport. I’m inspecting the current token model, API middleware, and admin API tests before changing it.


custom authentication guard request user resolver middleware

api middleware bearer token authentication testing


# Search Results

## Strings Fluent Strings `hash` {.collection-method}
Strings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md

#### `hash` {.collection-method}

The `hash` method hashes the string using the given [algorithm](https://www.php.net/manual/en/function.hash-algos.php):

```php
use Illuminate\Support\Str;

$hashed = Str::of('secret')->hash(algorithm: 'sha256');

// '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'
```

---

## Pest3 Now Available New Configuration API
Pest3 Now Available | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest3-now-available.md

## New Configuration API

Pest 1 / Pest 2's configuration API was a little bit confusing, the `uses()` function that was originally made only for having the `$this` variable within closure bound to the test case instance, ended up being used for pretty much everything.

In Pest 3, we've introduced a new configuration API that is more intuitive and easier to use. The new configuration API is based on the `pest()` function, which allows you to configure Pest using a fluent and expressive API.

> Note: the `uses()` function is still available in Pest 3, and we don't have plans to remove it. However, we recommend using the new configuration API for new projects.

```diff
-uses(TestCase::class)->in(__DIR__);
+pest()->extends(TestCase::class);

-uses(TestCase::class, RefreshDatabase::class)->in('Features');
+pest()->extends(TestCase::class)->use(RefreshDatabase::class)->in('Features');

-uses()->compact();
+pest()->printer()->compact();
```

And of course, any method that was available on the `uses()` API, like `->beforeEach()` or `->group()` is still available on the new `pest()` configuration API; we've just made it more intuitive and easier to use.

---

## Custom Helpers
Custom Helpers | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/custom-helpers.md

---
title: Custom Helpers
description: If you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.
---

# Custom Helpers

If you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.

For example, if your helper is specific to a certain test file, you may create the helper in the test file directly. Within your helper, you may invoke the `test()` function to access the test class instance that would normally be available via `$this`.

```php
use App\Models\User;
use Tests\TestCase;

function asAdmin(): TestCase
{
    $user = User::factory()->create([
        'admin' => true,
    ]);

    return test()->actingAs($user);
}

it('can manage users', function () {
    asAdmin()->get('/users')->assertOk();
})
```

> **Note:** If your helper creates a custom expectation, you should write a dedicated [custom expectation](/docs/custom-expectations) instead.

If your test helpers are utilized throughout your test suite, you may define them within the `tests/Pest.php` or `tests/Helpers.php` files. Alternatively, you can create a `tests/Helpers` directory to house your own helper files. All of these options will be automatically loaded by Pest.

```php
use App\Clients\PaymentClient;
use Mockery;

// tests/Pest.php or tests/Helpers.php
function mockPayments(): object
{
    $client = Mockery::mock(PaymentClient::class);

    //

    return $client;
}

// tests/Feature/PaymentsTest.php
it('may buy a book', function () {
    $client = mockPayments();

    //
})
```

As an alternative to defining helper methods as functions, you may define protected methods in your base test class and subsequently access them in your test cases using the `$this` variable.

```php
use App\Clients\PaymentClient;
use PHPUnit\Framework\TestCase as BaseTestCase;
use Mockery;

// tests/TestCase.php
class TestCase extends BaseTestCase
{
    protected function mockPayments(): void
    {
        $client = Mockery::mock(PaymentClient::class);

        //

        return $client;
    }
}

// tests/Pest.php
pest()->extend(TestCase::class)->in('Feature');

// tests/Feature/PaymentsTest.php
it('may buy a book', function () {
    $client = $this->mockPayments();

    //
})
```

---

In this section, we explored creating custom helpers. Digging deeper, you may even want to generate a custom expectation. Let's jump into that topic in the next chapter: [Custom Expectations](/docs/custom-expectations)

---

## Sanctum API Token Authentication Token Abilities Token Ability Middleware
Sanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md

#### Token Ability Middleware

Sanctum also includes two middleware that may be used to verify that an incoming request is authenticated with a token that has been granted a given ability. To get started, define the following middleware aliases in your application's `bootstrap/app.php` file:

```php
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'abilities' => CheckAbilities::class,
        'ability' => CheckForAnyAbility::class,
    ]);
})
```

The `abilities` middleware may be assigned to a route to verify that the incoming request's token has all of the listed abilities:

```php
Route::get('/orders', function () {
    // Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
```

The `ability` middleware may be assigned to a route to verify that the incoming request's token has *at least one* of the listed abilities:

```php
Route::get('/orders', function () {
    // Token has the "check-status" or "place-orders" ability...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);
```

---

## Hashing Basic Usage Hashing Passwords
Hashing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/hashing.md

### Hashing Passwords

You may hash a password by calling the `make` method on the `Hash` facade:

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class PasswordController extends Controller
{
    /**
     * Update the password for the user.
     */
    public function update(Request $request): RedirectResponse
    {
        // Validate the new password length...

        $request->user()->fill([
            'password' => Hash::make($request->newPassword)
        ])->save();

        return redirect('/profile');
    }
}
```

---

## Pagination Basic Usage Customizing Pagination URLs Appending Hash Fragments
Pagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md

#### Appending Hash Fragments

If you need to append a "hash fragment" to URLs generated by the paginator, you may use the `fragment` method. For example, to append `#users` to the end of each pagination link, you should invoke the `fragment` method like so:

```php
$users = User::paginate(15)->fragment('users');
```

---

## Cli Api Reference
Cli Api Reference | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/cli-api-reference.md

---
title: CLI API Reference
description: In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.
---

# CLI API Reference

In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.

## Configuration

- `--init`: Initialize a standard Pest configuration.
- `--bootstrap <file>` A PHP script that is included before the tests run.
- `-c|--configuration <file>`: Read configuration from XML file.
- `--no-configuration`: Ignore default configuration file (phpunit.xml).
- `--extension <class>`: Register test runner extension with bootstrap <class>.
- `--no-extensions`: Do not load PHPUnit extensions.
- `--include-path <path(s)>`: Prepend PHP's include_path with given path(s).
- `-d <key[=value]>`: Set a php.ini value.
- `--cache-directory <dir>`: Specify cache directory.
- `--generate-configuration`: Generate configuration file with suggested settings.
- `--migrate-configuration`: Migrate configuration file to current format.
- `--generate-baseline <file>`: Generate baseline for issues.
- `--use-baseline <file>`: Use baseline to ignore issues.
- `--ignore-baseline`: Do not use baseline to ignore issues.
- `--test-directory`: Specify test directory containing Pest.php, TestCase.php, helpers and your tests. Default: tests

## Selection

- `--bail`: Stop execution upon first not-passed test.
- `--ci`: Ignore focused tests using `->only()` and run the entire test suite.
- `--todos`: Output to standard output the list of todos.
- `--notes`: Output to standard output tests with notes.
- `--issue`: Output to standard output tests with the given issue number.
- `--pr`: Output to standard output tests with the given pull request number.
- `--pull-request`: Output to standard output tests with the given pull request number (alias for `--pr`).
- `--flaky`: Output to standard output tests marked as flaky.
- `--retry`: Run non-passing tests first and stop execution upon first error or failure.
- `--list-suites` List available test suites.
- `--testsuite <name>`: Only run tests from the specified test suite(s).
- `--exclude-testsuite <name>`: Exclude tests from the specified test suite(s).
- `--list-groups`: List available test groups.
- `--group <name>`: Only run tests from the specified group(s).
- `--exclude-group <name>`: Exclude tests from the specified group(s).
- `--covers <name>`: Only run tests that intend to cover `<name>`.
- `--uses <name>`: Only run tests that intend to use `<name>`.
- `--requires-php-extension <name>`: Only run tests that require PHP extension <name>.
- `--list-test-files`: List available test files.
- `--list-tests`: List available tests.
- `--list-tests-xml <file>`: List available tests in XML format.
- `--filter <pattern>`: Filter which tests to run
- `--exclude-filter <pattern>`: Exclude tests for the specified filter pattern.
- `--test-suffix <suffixes>`: Only search for test in files with specified suffix(es). Default: Test.php,.phpt

## Execution

- `--parallel` Run tests in parallel.
- `--shard <index/total>`: Run only the given shard of tests (e.g. `--shard=1/5`). Uses time-balanced distribution when `tests/.pest/shards.json` exists.
- `--update-shards`: Update `tests/.pest/shards.json` with test timing data for time-balanced sharding. Can be combined with `--parallel`.
- `--update-snapshots`: Update snapshots for tests using the "toMatchSnapshot" expectation.
- `--globals-backup`: Backup and restore $GLOBALS for each test.
- `--static-backup`: Backup and restore static properties for each test.
- `--strict-coverage`: Be strict about code coverage metadata.
- `--strict-global-state`: Be strict about changes to global state.
- `--disallow-test-output`: Be strict about output during tests.
- `--enforce-time-limit`: Enforce time limit based on test size.
- `--default-time-limit <sec>`: Timeout in seconds for tests that have no declared size.
- `--dont-report-useless-tests`: Do not report tests that do not test anything.
- `--stop-on-defect`: Stop execution upon first not-passed test.
- `--stop-on-error`: Stop execution upon first error.
- `--stop-on-failure`: Stop execution upon first error or failure.
- `--stop-on-warning`: Stop execution upon first warning.
- `--stop-on-risky`: Stop execution upon first risky test.
- `--stop-on-deprecation`: Stop after first test that triggered a deprecation.
- `--stop-on-notice`: Stop after first test that triggered a notice.
- `--stop-on-skipped`: Stop execution upon first skipped test.
- `--stop-on-incomplete`: Stop execution upon first incomplete test.
- `--fail-on-empty-test-suite`: Signal failure using shell exit code when no tests were run.
- `--fail-on-warning`: Treat tests with warnings as failures.
- `--fail-on-risky`: Treat risky tests as failures.
- `--fail-on-deprecation`: Signal failure using shell exit code when a deprecation was triggered.
- `--fail-on-phpunit-deprecation`: Signal failure using shell exit code when a PHPUnit deprecation was triggered.
- `--fail-on-notice`: Signal failure using shell exit code when a notice was triggered.
- `--fail-on-skipped`: Treat skipped tests as failures.
- `--fail-on-incomplete`: Signal failure using shell exit code when a test was marked incomplete.
- `--cache-result`: Write test results to cache file.
- `--do-not-cache-result`: Do not write test results to cache file
- `--order-by <order>`: Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size.
- `--random-order-seed <N>`: Use the specified random seed when running tests in random order

## Reporting

- `--colors <flag>`: Use colors in output ("never", "auto" or "always").
- `--columns <n>`: Number of columns to use for progress output.
- `--columns max`: Use maximum number of columns for progress output.
- `--stderr`: Write to STDERR instead of STDOUT.
- `--no-progress`: Disable output of test execution progress.
- `--no-results`: Disable output of test results.
- `--no-output`: Disable all output.
- `--display-incomplete`: Display details for incomplete tests.
- `--display-skipped`: Display details for skipped tests.
- `--display-deprecations`: Display details for deprecations triggered by tests.
- `--display-phpunit-deprecations`: Display details for PHPUnit deprecations.
- `--display-errors`: Display details for errors triggered by tests.
- `--display-notices`: Display details for notices triggered by tests.
- `--display-warnings`: Display details for warnings triggered by tests.
- `--reverse-list`: Print defects in reverse order.
- `--teamcity`: Replace default progress and result output with TeamCity format.
- `--testdox`: Replace default result output with TestDox format
- `--testdox-summary`: Repeat TestDox output for tests with errors, failures, or issues.
- `--debug`: Replace default progress and result output with debugging information.
- `--compact`:  Replace default result output with Compact format

## Logging

- `--log-junit <file>`: Write test results in JUnit XML format to file.
- `--log-teamcity <file>`: Write test results in TeamCity format to file.
- `--testdox-html <file>`: Write test results in TestDox format (HTML) to file.
- `--testdox-text <file>`: Write test results in TestDox format (plain text) to file.
- `--log-events-text <file>`: Stream events as plain text to file.
- `--log-events-verbose-text <file>`: Stream events as plain text (with telemetry information) to file.
- `--no-logging`: Ignore logging configured in the XML configuration file

## Code Coverage

- `--coverage`: Generate code coverage report and output to standard output.
- `--coverage --min=<value>`: Set the minimum required coverage percentage, and fail if not met.
- `--coverage --exactly=<value>`: Set the exact required coverage percentage, and fail if not met.
- `--coverage --only-covered`: Hide files with 0% coverage from the code coverage report.
- `--coverage-clover <file>`: Write code coverage report in Clover XML format to file.
- `--coverage-cobertura <file>`: Write code coverage report in Cobertura XML format to file.
- `--coverage-crap4j <file>`: Write code coverage report in Crap4J XML format to file.
- `--coverage-html <dir>`: Write code coverage report in HTML format to directory.
- `--coverage-php <file>`: Write serialized code coverage data to file.
- `--coverage-text=<file>`: Write code coverage report in text format to file [default: standard output].
- `--only-summary-for-coverage-text`: Option for code coverage report in text format: only show summary.
- `--show-uncovered-for-coverage-text`: Option for code coverage report in text format: show uncovered files.
- `--coverage-xml <dir>`: Write code coverage report in XML format to directory.
- `--warm-coverage-cache`: Warm static analysis cache.
- `--coverage-filter <dir>`: Include `<dir>`: in code coverage reporting.
- `--path-coverage`: Report path coverage in addition to line coverage.
- `--disable-coverage-ignore`: Disable metadata for ignoring code coverage.
- `--no-coverage`: Ignore code coverage reporting configured in the XML configuration file

## Mutation Testing

- `--mutate`: Runs mutation testing, to understand the quality of your tests.
- `--mutate --parallel`: Runs mutation testing in parallel.
- `--mutate --min`: Set the minimum required mutation score, and fail if not met.
- `--mutate --id`:  Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run.
- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.
- `--mutate --bail`: Stop mutation testing execution upon first untested or uncovered mutation.
- `--mutate --class`: Generate mutations for the given class(es). E.g. --class=App\\Models.
- `--mutate --ignore`: Ignore the given class(es) when generating mutations. E.g. --ignore=App\\Http\\Requests.
- `--mutate --clear-cache`: Clear the mutation cache.
- `--mutate --no-cache`: Clear the mutation cache.
- `--mutate --ignore-min-score-on-zero-mutations`: Ignore the minimum score requirement when there are no mutations.
- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.
- `--mutate --everything`: Generate mutations for all classes, even if they are not covered by tests.
- `--mutate --profile`: Output to standard output the top ten slowest mutations.
- `--mutate --retry`: Run untested or uncovered mutations first and stop execution upon first error or failure.
- `--mutate --stop-on-uncovered`: Stop mutation testing execution upon first untested mutation.
- `--mutate --stop-on-untested`: Stop mutation testing execution upon first untested mutation.

## Profiling

- `--profile`: Output to standard output the top ten slowest tests

---

In this chapter, you found a complete list of CLI options provided by Pest. In the subsequent documentation, we will explore the topic of test dependencies: [Test Dependencies](/docs/test-dependencies)

---

## Custom Expectations
Custom Expectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/custom-expectations.md

---
title: Custom Expectations
description: Pest's expectation API is powerful by default, but there may be times when you need to write the same expectations repeatedly between tests. In such cases, creating custom expectations that meet your specific requirements can be incredibly useful.
---

# Custom Expectations

Pest's expectation API is powerful by default, but there may be times when you need to write the same expectations repeatedly between tests. In such cases, creating custom expectations that meet your specific requirements can be incredibly useful.

Custom expectations are usually defined in the `tests/Pest.php` file, but you can also organize them in a separate `tests/Expectations.php` file for better maintainability. To create a custom expectation in Pest, chain the `extend()` method onto the `expect()` function without providing any expectation value.

For example, suppose you are testing a number utility library and you need to frequently assert that numbers fall within a given range. In this case, you might create a custom expectation called `toBeWithinRange()`:

```php
// Pest.php or Expectations.php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
    return $this->toBeGreaterThanOrEqual($min)
                ->toBeLessThanOrEqual($max);
});

// Tests/Unit/ExampleTest.php
test('numeric ranges', function () {
    expect(100)->toBeWithinRange(90, 110);
});
```

While users typically utilize Pest's built-in expectations within their custom expectations as demonstrated in the `toBeWithinRange()` example, there may be times when you need to access the expectation value directly to perform your own custom expectation logic. In such cases, you can access the expectation value that was passed to `expect($value)` via the `$this->value` property.

```php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
    echo $this->value; // 100
});
```

Of course, you probably want users to have the ability to "chain" expectations together with your custom expectation. To achieve this, ensure your custom expectation includes a `return $this` statement.

```php
// Pest.php or Expectations.php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
    // Assertions based on `$this->value` and the given arguments...

    return $this; // Return this, so another expectations can chain this one...
});

// Tests/Unit/ExampleTest.php
test('numeric ranges', function () {
    expect(100)
        ->toBeInt()
        ->toBeWithinRange(90, 110)
        ->to...
});
```

Sometimes, you may need to trigger a test failure in your Custom Expectation. To do so, use the `test()` method in combination with the [`fail()`](/docs/exceptions) method.

```php
// Pest.php or Expectations.php
expect()->extend('toBeDivisibleBy', function (int $divisor) {
    if ($divisor === 0) {
        test()->fail('The divisor cannot be 0.');
    }

    return expect($this->value % $divisor)->toBe(0);
});

// Tests/Unit/ExampleTest.php
test('numeral division', function () {
    expect(10)->toBeDivisibleBy(2); // Pass
    expect(10)->toBeDivisibleBy(0); // Fail "The divisor cannot be 0."
});
```

## Intercept Expectations

Although it is considered an advanced practice, you can override existing expectations with your own implementation via the `intercept()` method. When using this method, the existing expectation will be fully substituted if the expectation value is of the specified type. For example, you can replace the `toBe()` expectation to check if two objects of the `Illuminate\Database\Eloquent\Model` type have the same `id`.

```php
use Illuminate\Database\Eloquent\Model;
use App\Models\User;

// tests/Pest.php or tests/Expectations.php
expect()->intercept('toBe', Model::class, function(Model $expected) {
    expect($this->value->id)->toBe($expected->id);
});

// tests/Feature/ExampleTest.php
test('models', function () {
    $userA = User::find(1);
    $userB = User::find(1);

    expect($userA)->toBe($userB);
});
```

Instead of passing a string type as the second argument to the `intercept()` method, you may also pass a closure, which will be invoked to determine whether or not to override the core expectation.

```php
expect()->intercept('toBe', fn (mixed $value) => is_string($value), function (string $expected, bool $ignoreCase = false) {
    if ($ignoreCase) {
        assertEqualsIgnoringCase($expected, $this->value);
    } else {
        assertSame($expected, $this->value);
    }
});
```

## Pipe Expectations

There may be instances where you want to run one of Pest's built-in expectations, but include customized expectation logic under certain conditions. In these cases, you can use the `pipe()` method. For example, we may want to customize the behavior of the `toBe()` expectation if the given value is an Eloquent model.

```php
use Illuminate\Database\Eloquent\Model;
use App\Models\User;

expect()->pipe('toBe', function (Closure $next, mixed $expected) {
    if ($this->value instanceof Model) {
        return expect($this->value->id)->toBe($expected->id);
    }

    return $next(); // Run to the original, built-in expectation...
});
```

---

As demonstrated, creating custom expectations can significantly simplify your code by eliminating the need to duplicate the logic to verify that your tests are behaving as anticipated. In the following chapter, we will explore how Pest can scan for Profanity, to help you maintain a professional codebase: [Profanity Plugin](/docs/profanity)

_[Truncated]_