status: completed

input:
*** Begin Patch
*** Update File: specs/progress.md
@@
 - 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminShippingSettingsApiTest.php` passed after the admin shipping settings API changes: 3 tests, 27 assertions.
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin shipping settings API changes: 45 tests, 364 assertions.
 - 2026-05-04: `php artisan route:list --path=api/admin/v1/stores --except-vendor` confirmed 29 admin API routes, including shipping zone list/create/update and rate create.
 - 2026-05-04: `php artisan test --compact` passed after the admin shipping settings API changes: 232 tests, 1321 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 file upload validation, scoped route model binding, JSON API testing, and Pest 4 docs before the admin theme API changes.
+- 2026-05-04: `php artisan make:class Services/ThemeArchiveInstaller --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeController --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction`, `php artisan make:resource Admin/V1/ThemeResource --no-interaction`, and `php artisan make:test Api/AdminThemeApiTest --pest --no-interaction` created the theme archive installer, API controllers/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin theme API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminThemeApiTest.php` passed after the admin theme API changes: 4 tests, 29 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin theme API changes: 49 tests, 393 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/themes --except-vendor` confirmed 3 theme API routes: upload/install, publish, and settings update.
+- 2026-05-04: `php artisan test --compact` passed after the admin theme API changes: 236 tests, 1350 assertions.
@@
 - The tax settings API accepts the spec-facing `default_tax_rate`/`tax_rates` payload shape and normalizes it to the existing internal `default_rate_bps`/`rates` config consumed by the calculator and admin UI.
 - The shipping settings API accepts spec-facing `price_amount`/`tiers` rate config keys and normalizes them to the existing `amount`/`ranges` config consumed by `ShippingCalculator`.
+- The theme upload API imports ZIP archives with `theme.json` or `manifest.json`, requires the same core template paths seeded for default themes, stores each imported file on the local disk under the new theme id, and seeds theme settings from the manifest or the store defaults.
+- The theme publish API validates required files before switching the store to exactly one published theme and flushing cached theme settings.
 - 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 REST surfaces for general settings, themes, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for general settings and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
@@
-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, product/customer/collection/discount/content page/search/analytics/shipping/tax settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
*** End Patch


stdout:
Success. Updated the following files:
M specs/progress.md

type: update

unified_diff:
@@ -382,2 +382,9 @@
 - 2026-05-04: `php artisan test --compact` passed after the admin shipping settings API changes: 232 tests, 1321 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 file upload validation, scoped route model binding, JSON API testing, and Pest 4 docs before the admin theme API changes.
+- 2026-05-04: `php artisan make:class Services/ThemeArchiveInstaller --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeController --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction`, `php artisan make:resource Admin/V1/ThemeResource --no-interaction`, and `php artisan make:test Api/AdminThemeApiTest --pest --no-interaction` created the theme archive installer, API controllers/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin theme API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminThemeApiTest.php` passed after the admin theme API changes: 4 tests, 29 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin theme API changes: 49 tests, 393 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/themes --except-vendor` confirmed 3 theme API routes: upload/install, publish, and settings update.
+- 2026-05-04: `php artisan test --compact` passed after the admin theme API changes: 236 tests, 1350 assertions.

@@ -423,2 +430,4 @@
 - The shipping settings API accepts spec-facing `price_amount`/`tiers` rate config keys and normalizes them to the existing `amount`/`ranges` config consumed by `ShippingCalculator`.
+- The theme upload API imports ZIP archives with `theme.json` or `manifest.json`, requires the same core template paths seeded for default themes, stores each imported file on the local disk under the new theme id, and seeds theme settings from the manifest or the store defaults.
+- The theme publish API validates required files before switching the store to exactly one published theme and flushing cached theme settings.
 - 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.
@@ -449,3 +458,3 @@
 - `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.
-- Admin REST surfaces for general settings, themes, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for general settings and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
@@ -455,2 +464,2 @@

-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, product/customer/collection/discount/content page/search/analytics/shipping/tax settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.

status: completed


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


command:
/bin/zsh
-lc
git status --short

type: unknown

cmd: git status --short

aggregated_output:
 M routes/api.php
 M specs/progress.md
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T05-58-46-727Z.log
?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


output:
Chunk ID: bf30c3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 116
Output:
 M routes/api.php
 M specs/progress.md
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T05-58-46-727Z.log
?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


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


cmd: php artisan route:list --path=api/admin/v1/stores --except-vendor


command:
/bin/zsh
-lc
git diff -- specs/progress.md

cmd: git diff -- specs/progress.md

aggregated_output:
diff --git a/specs/progress.md b/specs/progress.md
index 71ad6bad..293e0714 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -380,6 +380,13 @@ ## Verification Evidence
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin shipping settings API changes: 45 tests, 364 assertions.
 - 2026-05-04: `php artisan route:list --path=api/admin/v1/stores --except-vendor` confirmed 29 admin API routes, including shipping zone list/create/update and rate create.
 - 2026-05-04: `php artisan test --compact` passed after the admin shipping settings API changes: 232 tests, 1321 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 file upload validation, scoped route model binding, JSON API testing, and Pest 4 docs before the admin theme API changes.
+- 2026-05-04: `php artisan make:class Services/ThemeArchiveInstaller --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeController --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction`, `php artisan make:resource Admin/V1/ThemeResource --no-interaction`, and `php artisan make:test Api/AdminThemeApiTest --pest --no-interaction` created the theme archive installer, API controllers/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin theme API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminThemeApiTest.php` passed after the admin theme API changes: 4 tests, 29 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin theme API changes: 49 tests, 393 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/themes --except-vendor` confirmed 3 theme API routes: upload/install, publish, and settings update.
+- 2026-05-04: `php artisan test --compact` passed after the admin theme API changes: 236 tests, 1350 assertions.

 ## Decisions

@@ -421,6 +428,8 @@ ## Decisions
 - 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.
 - The tax settings API accepts the spec-facing `default_tax_rate`/`tax_rates` payload shape and normalizes it to the existing internal `default_rate_bps`/`rates` config consumed by the calculator and admin UI.
 - The shipping settings API accepts spec-facing `price_amount`/`tiers` rate config keys and normalizes them to the existing `amount`/`ranges` config consumed by `ShippingCalculator`.
+- The theme upload API imports ZIP archives with `theme.json` or `manifest.json`, requires the same core template paths seeded for default themes, stores each imported file on the local disk under the new theme id, and seeds theme settings from the manifest or the store defaults.
+- The theme publish API validates required files before switching the store to exactly one published theme and flushing cached theme settings.
 - 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 nested ordered menu items with Livewire `wire:sort`, up/down sibling controls, and a parent selector; children are saved under their selected top-level parent with per-parent positions.
 - The theme editor uses the `ThemeSettingsService` default settings shape as the editable schema and writes back to `theme_settings.settings_json`.
@@ -447,10 +456,10 @@ ## Open Issues

 - 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.
-- Admin REST surfaces for general settings, themes, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for general settings and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
 - 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, product/customer/collection/discount/content page/search/analytics/shipping/tax settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.


output:
Chunk ID: 8fab0c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1714
Output:
diff --git a/specs/progress.md b/specs/progress.md
index 71ad6bad..293e0714 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -380,6 +380,13 @@ ## Verification Evidence
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin shipping settings API changes: 45 tests, 364 assertions.
 - 2026-05-04: `php artisan route:list --path=api/admin/v1/stores --except-vendor` confirmed 29 admin API routes, including shipping zone list/create/update and rate create.
 - 2026-05-04: `php artisan test --compact` passed after the admin shipping settings API changes: 232 tests, 1321 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 file upload validation, scoped route model binding, JSON API testing, and Pest 4 docs before the admin theme API changes.
+- 2026-05-04: `php artisan make:class Services/ThemeArchiveInstaller --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeController --no-interaction`, `php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction`, `php artisan make:resource Admin/V1/ThemeResource --no-interaction`, and `php artisan make:test Api/AdminThemeApiTest --pest --no-interaction` created the theme archive installer, API controllers/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin theme API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminThemeApiTest.php` passed after the admin theme API changes: 4 tests, 29 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin theme API changes: 49 tests, 393 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/themes --except-vendor` confirmed 3 theme API routes: upload/install, publish, and settings update.
+- 2026-05-04: `php artisan test --compact` passed after the admin theme API changes: 236 tests, 1350 assertions.

 ## Decisions

@@ -421,6 +428,8 @@ ## Decisions
 - 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.
 - The tax settings API accepts the spec-facing `default_tax_rate`/`tax_rates` payload shape and normalizes it to the existing internal `default_rate_bps`/`rates` config consumed by the calculator and admin UI.
 - The shipping settings API accepts spec-facing `price_amount`/`tiers` rate config keys and normalizes them to the existing `amount`/`ranges` config consumed by `ShippingCalculator`.
+- The theme upload API imports ZIP archives with `theme.json` or `manifest.json`, requires the same core template paths seeded for default themes, stores each imported file on the local disk under the new theme id, and seeds theme settings from the manifest or the store defaults.
+- The theme publish API validates required files before switching the store to exactly one published theme and flushing cached theme settings.
 - 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 nested ordered menu items with Livewire `wire:sort`, up/down sibling controls, and a parent selector; children are saved under their selected top-level parent with per-parent positions.
 - The theme editor uses the `ThemeSettingsService` default settings shape as the editable schema and writes back to `theme_settings.settings_json`.
@@ -447,10 +456,10 @@ ## Open Issues

 - 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.
-- Admin REST surfaces for general settings, themes, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for general settings and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
 - 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, product/customer/collection/discount/content page/search/analytics/shipping/tax settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.


aggregated_output:
 M routes/api.php
 M specs/progress.md
?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


output:
Chunk ID: 5a6fd9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 74
Output:
 M routes/api.php
 M specs/progress.md
?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


output:
Chunk ID: d92e9d
Wall time: 0.2138 seconds
Process exited with code 0
Original token count: 671
Output:

  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…

                                                           Showing [32] routes


command: php artisan route:list --path=api/admin/v1/stores --except-vendor

aggregated_output:
  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…

                                                           Showing [32] routes


status: completed

input:
*** Begin Patch
*** Update File: specs/progress.md
@@
-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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
*** End Patch


unified_diff:
@@ -464,2 +464,2 @@

-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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme settings/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+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, product/customer/collection/discount/content page/search/analytics/shipping/tax/theme/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.


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


command:
/bin/zsh
-lc
git diff --check

cmd: git diff --check


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


cmd: git add routes/api.php specs/progress.md app/Http/Controllers/Api/Admin/V1/ThemeController.php app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php app/Http/Resources/Admin/V1/ThemeResource.php app/Services/ThemeArchiveInstaller.php tests/Feature/Api/AdminThemeApiTest.php && git diff --cached --stat


command: git add routes/api.php specs/progress.md app/Http/Controllers/Api/Admin/V1/ThemeController.php app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php app/Http/Resources/Admin/V1/ThemeResource.php app/Services/ThemeArchiveInstaller.php tests/Feature/Api/AdminThemeApiTest.php && git diff --cached --stat

aggregated_output:
 .../Controllers/Api/Admin/V1/ThemeController.php   |  93 ++++++++
 .../Api/Admin/V1/ThemeSettingsController.php       |  57 +++++
 app/Http/Resources/Admin/V1/ThemeResource.php      |  30 +++
 app/Services/ThemeArchiveInstaller.php             | 244 +++++++++++++++++++++
 routes/api.php                                     |   8 +
 specs/progress.md                                  |  13 +-
 tests/Feature/Api/AdminThemeApiTest.php            | 211 ++++++++++++++++++
 7 files changed, 654 insertions(+), 2 deletions(-)


output:
Chunk ID: b4aa40
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 131
Output:
 .../Controllers/Api/Admin/V1/ThemeController.php   |  93 ++++++++
 .../Api/Admin/V1/ThemeSettingsController.php       |  57 +++++
 app/Http/Resources/Admin/V1/ThemeResource.php      |  30 +++
 app/Services/ThemeArchiveInstaller.php             | 244 +++++++++++++++++++++
 routes/api.php                                     |   8 +
 specs/progress.md                                  |  13 +-
 tests/Feature/Api/AdminThemeApiTest.php            | 211 ++++++++++++++++++
 7 files changed, 654 insertions(+), 2 deletions(-)


aggregated_output:
A  app/Http/Controllers/Api/Admin/V1/ThemeController.php
A  app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
A  app/Http/Resources/Admin/V1/ThemeResource.php
A  app/Services/ThemeArchiveInstaller.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminThemeApiTest.php


output:
Chunk ID: 1bd3b8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 74
Output:
A  app/Http/Controllers/Api/Admin/V1/ThemeController.php
A  app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
A  app/Http/Resources/Admin/V1/ThemeResource.php
A  app/Services/ThemeArchiveInstaller.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminThemeApiTest.php


message: The staged slice contains only the theme API files, routes, tests, and progress notes. I’m committing it now, then I’ll continue to the next remaining REST gap.

phase: commentary


type: message

role: assistant

text: The staged slice contains only the theme API files, routes, tests, and progress notes. I’m committing it now, then I’ll continue to the next remaining REST gap.


cmd: git commit -m "Add admin theme API endpoints"


command: git commit -m "Add admin theme API endpoints"

aggregated_output:
[2026-05-04-codex-5-5-goal b61e6322] Add admin theme API endpoints
 7 files changed, 654 insertions(+), 2 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Admin/V1/ThemeController.php
 create mode 100644 app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
 create mode 100644 app/Http/Resources/Admin/V1/ThemeResource.php
 create mode 100644 app/Services/ThemeArchiveInstaller.php
 create mode 100644 tests/Feature/Api/AdminThemeApiTest.php


output:
Chunk ID: 93e76d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 115
Output:
[2026-05-04-codex-5-5-goal b61e6322] Add admin theme API endpoints
 7 files changed, 654 insertions(+), 2 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Admin/V1/ThemeController.php
 create mode 100644 app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
 create mode 100644 app/Http/Resources/Admin/V1/ThemeResource.php
 create mode 100644 app/Services/ThemeArchiveInstaller.php
 create mode 100644 tests/Feature/Api/AdminThemeApiTest.php


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


message: Committed `b61e6322 Add admin theme API endpoints`; the worktree is clean. The remaining admin REST gap called out in progress is now general settings and exports, so I’m checking the specs and existing settings models/UI to pick the next narrow slice.


text: Committed `b61e6322 Add admin theme API endpoints`; the worktree is clean. The remaining admin REST gap called out in progress is now general settings and exports, so I’m checking the specs and existing settings models/UI to pick the next narrow slice.


packages: laravel/framework pestphp/pest

queries:
json api update nested array validation
update or create has one settings model
json api testing validation errors


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Eloquent Inserting and Updating Models Updates
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

### Updates

The `save` method may also be used to update models that already exist in the database. To update a model, you should retrieve it and set any attributes you wish to update. Then, you should call the model's `save` method. Again, the `updated_at` timestamp will automatically be updated, so there is no need to manually set its value:

```php
use App\Models\Flight;

$flight = Flight::find(1);

$flight->name = 'Paris to London';

$flight->save();
```

Occasionally, you may need to update an existing model or create a new model if no matching model exists. Like the `firstOrCreate` method, the `updateOrCreate` method persists the model, so there's no need to manually call the `save` method.

In the example below, if a flight exists with a `departure` location of `Oakland` and a `destination` location of `San Diego`, its `price` and `discounted` columns will be updated. If no such flight exists, a new flight will be created which has the attributes resulting from merging the first argument array with the second argument array:

```php
$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);
```

When using methods such as `firstOrCreate` or `updateOrCreate`, you may not know whether a new model has been created or an existing one has been updated. The `wasRecentlyCreated` property indicates if the model was created during its current lifecycle:

```php
$flight = Flight::updateOrCreate(
    // ...
);

if ($flight->wasRecentlyCreated) {
    // New flight record was inserted...
}
```

---

## Expectations `json()`
Expectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md

### `json()`

The `json()` modifier decodes the current expectation `$value` from JSON to an array.

```php
expect('{"name":"Nuno","credit":1000.00}')
    ->json()
    ->toHaveCount(2)
    ->name->toBe('Nuno')
    ->credit->toBeFloat();

expect('not-a-json')->json(); //Fails
```

---

## Queries Update Statements Updating JSON Columns
Queries | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queries.md

### Updating JSON Columns

When updating a JSON column, you should use `->` syntax to update the appropriate key in the JSON object. This operation is supported on MariaDB 10.3+, MySQL 5.7+, and PostgreSQL 9.5+:

```php
$affected = DB::table('users')
    ->where('id', 1)
    ->update(['options->enabled' => true]);
```

---

## Expectations `toBeJson()`
Expectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md

### `toBeJson()`

This expectation ensures that `$value` is a JSON string.

```php
expect('{"hello":"world"}')->toBeJson();
```

---

## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

## Validation Available Validation Rules json
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### json

The field under validation must be a valid JSON string.

---

## Eloquent Inserting and Updating Models
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

## Inserting and Updating Models

---

## Http Tests Testing JSON APIs
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

## Testing JSON APIs

Laravel also provides several helpers for testing JSON APIs and their responses. For example, the `json`, `getJson`, `postJson`, `putJson`, `patchJson`, `deleteJson`, and `optionsJson` methods may be used to issue JSON requests with various HTTP verbs. You may also easily pass data and headers to these methods. To get started, let's write a test to make a `POST` request to `/api/user` and assert that the expected JSON data was returned:

```php tab=Pest
<?php

test('making an api request', function () {
    $response = $this->postJson('/api/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertJson([
            'created' => true,
        ]);
});
```

```php tab=PHPUnit
<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_making_an_api_request(): void
    {
        $response = $this->postJson('/api/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertJson([
                'created' => true,
            ]);
    }
}
```

In addition, JSON response data may be accessed as array variables on the response, making it convenient for you to inspect the individual values returned within a JSON response:

```php tab=Pest
expect($response['created'])->toBeTrue();
```

```php tab=PHPUnit
$this->assertTrue($response['created']);
```

> [!NOTE]
> The `assertJson` method converts the response to an array to verify that the given array exists within the JSON response returned by the application. So, if there are other properties in the JSON response, this test will still pass as long as the given fragment is present.

---

## Upgrade Upgrading To 12.0 From 11.x Updating the Laravel Installer
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

### Updating the Laravel Installer

If you are using the Laravel installer CLI tool to create new Laravel applications, you should update your installer installation to be compatible with Laravel 12.x and the [new Laravel starter kits](https://laravel.com/starter-kits). If you installed the Laravel installer via `composer global require`, you may update the installer using `composer global update`:

```shell
composer global update laravel/installer
```

If you originally installed PHP and Laravel via `php.new`, you may simply re-run the `php.new` installation commands for your operating system to install the latest version of PHP and the Laravel installer:

```shell tab=macOS
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.4)"
```

```shell tab=Windows PowerShell
# Run as administrator...
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4'))
```

```shell tab=Linux
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.4)"
```

Or, if you are using [Laravel Herd's](https://herd.laravel.com) bundled copy of the Laravel installer, you should update your Herd installation to the latest release.

---

## Http Tests Available Assertions Response Assertions assertJsonValidationErrors
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertJsonValidationErrors

Assert that the response has the given JSON validation errors for the given keys. This method should be used when asserting against responses where the validation errors are returned as a JSON structure instead of being flashed to the session:

```php
$response->assertJsonValidationErrors(array $data, $responseKey = 'errors');
```

> [!NOTE]
> The more generic [assertInvalid](#assert-invalid) method may be used to assert that a response has validation errors returned as JSON **or** that errors were flashed to session storage.

---

## Upgrade Upgrading To 12.0 From 11.x Updating Dependencies Carbon 3
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Carbon 3

**Likelihood Of Impact: Low**

Support for Carbon 2.x has been removed. All Laravel 12 applications now require [Carbon 3.x](https://carbon.nesbot.com/guide/getting-started/migration.html).

---

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

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

The `isJson` method determines if a given string is valid JSON:

```php
use Illuminate\Support\Str;

$result = Str::of('[1,2,3]')->isJson();

// true

$result = Str::of('{"first": "John", "last": "Doe"}')->isJson();

// true

$result = Str::of('{first: "John", last: "Doe"}')->isJson();

// false
```

---

## Eloquent Relationships Inserting and Updating Related Models
Eloquent Relationships | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-relationships.md

## Inserting and Updating Related Models

---

## Eloquent Inserting and Updating Models Mass Assignment Mass Assignment and JSON Columns
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

#### Mass Assignment and JSON Columns

When assigning JSON columns, each column's mass assignable key must be specified in your model's `$fillable` array. For security, Laravel does not support updating nested JSON attributes when using the `guarded` property:

```php
/**
 * The attributes that are mass assignable.
 *
 * @var array<int, string>
 */
protected $fillable = [
    'options->enabled',
];
```

---

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

#### `Str::isJson()` {.collection-method}

The `Str::isJson` method determines if the given string is valid JSON:

```php
use Illuminate\Support\Str;

$result = Str::isJson('[1,2,3]');

// true

$result = Str::isJson('{"first": "John", "last": "Doe"}');

// true

$result = Str::isJson('{first: "John", last: "Doe"}');

// false
```

---

## Upgrade Upgrading To 12.0 From 11.x Updating Dependencies
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

### Updating Dependencies

**Likelihood Of Impact: High**

You should update the following dependencies in your application's `composer.json` file:

- `laravel/framework` to `^12.0`
- `phpunit/phpunit` to `^11.0`
- `pestphp/pest` to `^3.0`

---

## Http Tests Available Assertions Response Assertions assertJsonMissingValidationErrors
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertJsonMissingValidationErrors

Assert that the response has no JSON validation errors for the given keys:

```php
$response->assertJsonMissingValidationErrors($keys);
```

> [!NOTE]
> The more generic [assertValid](#assert-valid) method may be used to assert that a response does not have validation errors that were returned as JSON **and** that no errors were flashed to session storage.

---

## Scout Third-Party Engine Indexing Updating Records Conditionally Updating the Search Index
Scout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md

#### Conditionally Updating the Search Index

By default, Scout will reindex an updated model regardless of which attributes were modified. If you would like to customize this behavior, you may define a `searchIndexShouldBeUpdated` method on your model:

```php
/**
 * Determine if the search index should be updated.
 */
public function searchIndexShouldBeUpdated(): bool
{
    return $this->wasRecentlyCreated || $this->wasChanged(['title', 'body']);
}
```

_[Truncated]_


output:
Wall time: 3.3360 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Eloquent Inserting and Updating Models Updates\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n### Updates\n\nThe `save` method may also be used to update models that already exist in the database. To update a model, you should retrieve it and set any attributes you wish to update. Then, you should call the model's `save` method. Again, the `updated_at` timestamp will automatically be updated, so there is no need to manually set its value:\n\n```php\nuse App\\Models\\Flight;\n\n$flight = Flight::find(1);\n\n$flight->name = 'Paris to London';\n\n$flight->save();\n```\n\nOccasionally, you may need to update an existing model or create a new model if no matching model exists. Like the `firstOrCreate` method, the `updateOrCreate` method persists the model, so there's no need to manually call the `save` method.\n\nIn the example below, if a flight exists with a `departure` location of `Oakland` and a `destination` location of `San Diego`, its `price` and `discounted` columns will be updated. If no such flight exists, a new flight will be created which has the attributes resulting from merging the first argument array with the second argument array:\n\n```php\n$flight = Flight::updateOrCreate(\n    ['departure' => 'Oakland', 'destination' => 'San Diego'],\n    ['price' => 99, 'discounted' => 1]\n);\n```\n\nWhen using methods such as `firstOrCreate` or `updateOrCreate`, you may not know whether a new model has been created or an existing one has been updated. The `wasRecentlyCreated` property indicates if the model was created during its current lifecycle:\n\n```php\n$flight = Flight::updateOrCreate(\n    // ...\n);\n\nif ($flight->wasRecentlyCreated) {\n    // New flight record was inserted...\n}\n```\n\n---\n\n## Expectations `json()`\nExpectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md\n\n\n### `json()`\n\nThe `json()` modifier decodes the current expectation `$value` from JSON to an array.\n\n```php\nexpect('{\"name\":\"Nuno\",\"credit\":1000.00}')\n    ->json()\n    ->toHaveCount(2)\n    ->name->toBe('Nuno')\n    ->credit->toBeFloat();\n\nexpect('not-a-json')->json(); //Fails\n```\n\n---\n\n## Queries Update Statements Updating JSON Columns\nQueries | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queries.md\n\n\n### Updating JSON Columns\n\nWhen updating a JSON column, you should use `->` syntax to update the appropriate key in the JSON object. This operation is supported on MariaDB 10.3+, MySQL 5.7+, and PostgreSQL 9.5+:\n\n```php\n$affected = DB::table('users')\n    ->where('id', 1)\n    ->update(['options->enabled' => true]);\n```\n\n---\n\n## Expectations `toBeJson()`\nExpectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md\n\n\n### `toBeJson()`\n\nThis expectation ensures that `$value` is a JSON string.\n\n```php\nexpect('{\"hello\":\"world\"}')->toBeJson();\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Updated `DatabaseTokenRepository` Constructor Signature\n\n**Likelihood Of Impact: Very Low**\n\nThe constructor of the `Illuminate\\Auth\\Passwords\\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.\n\n---\n\n## Validation Available Validation Rules json\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### json\n\nThe field under validation must be a valid JSON string.\n\n---\n\n## Eloquent Inserting and Updating Models\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n## Inserting and Updating Models\n\n---\n\n## Http Tests Testing JSON APIs\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n## Testing JSON APIs\n\nLaravel also provides several helpers for testing JSON APIs and their responses. For example, the `json`, `getJson`, `postJson`, `putJson`, `patchJson`, `deleteJson`, and `optionsJson` methods may be used to issue JSON requests with various HTTP verbs. You may also easily pass data and headers to these methods. To get started, let's write a test to make a `POST` request to `/api/user` and assert that the expected JSON data was returned:\n\n```php tab=Pest\n<?php\n\ntest('making an api request', function () {\n    $response = $this->postJson('/api/user', ['name' => 'Sally']);\n\n    $response\n        ->assertStatus(201)\n        ->assertJson([\n            'created' => true,\n        ]);\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    /**\n     * A basic functional test example.\n     */\n    public function test_making_an_api_request(): void\n    {\n        $response = $this->postJson('/api/user', ['name' => 'Sally']);\n\n        $response\n            ->assertStatus(201)\n            ->assertJson([\n                'created' => true,\n            ]);\n    }\n}\n```\n\nIn addition, JSON response data may be accessed as array variables on the response, making it convenient for you to inspect the individual values returned within a JSON response:\n\n```php tab=Pest\nexpect($response['created'])->toBeTrue();\n```\n\n```php tab=PHPUnit\n$this->assertTrue($response['created']);\n```\n\n> [!NOTE]\n> The `assertJson` method converts the response to an array to verify that the given array exists within the JSON response returned by the application. So, if there are other properties in the JSON response, this test will still pass as long as the given fragment is present.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Updating the Laravel Installer\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n### Updating the Laravel Installer\n\nIf you are using the Laravel installer CLI tool to create new Laravel applications, you should update your installer installation to be compatible with Laravel 12.x and the [new Laravel starter kits](https://laravel.com/starter-kits). If you installed the Laravel installer via `composer global require`, you may update the installer using `composer global update`:\n\n```shell\ncomposer global update laravel/installer\n```\n\nIf you originally installed PHP and Laravel via `php.new`, you may simply re-run the `php.new` installation commands for your operating system to install the latest version of PHP and the Laravel installer:\n\n```shell tab=macOS\n/bin/bash -c \"$(curl -fsSL https://php.new/install/mac/8.4)\"\n```\n\n```shell tab=Windows PowerShell\n# Run as administrator...\nSet-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4'))\n```\n\n```shell tab=Linux\n/bin/bash -c \"$(curl -fsSL https://php.new/install/linux/8.4)\"\n```\n\nOr, if you are using [Laravel Herd's](https://herd.laravel.com) bundled copy of the Laravel installer, you should update your Herd installation to the latest release.\n\n---\n\n## Http Tests Available Assertions Response Assertions assertJsonValidationErrors\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertJsonValidationErrors\n\nAssert that the response has the given JSON validation errors for the given keys. This method should be used when asserting against responses where the validation errors are returned as a JSON structure instead of being flashed to the session:\n\n```php\n$response->assertJsonValidationErrors(array $data, $responseKey = 'errors');\n```\n\n> [!NOTE]\n> The more generic [assertInvalid](#assert-invalid) method may be used to assert that a response has validation errors returned as JSON **or** that errors were flashed to session storage.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Updating Dependencies Carbon 3\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Carbon 3\n\n**Likelihood Of Impact: Low**\n\nSupport for Carbon 2.x has been removed. All Laravel 12 applications now require [Carbon 3.x](https://carbon.nesbot.com/guide/getting-started/migration.html).\n\n---\n\n## Strings Fluent Strings `isJson` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `isJson` {.collection-method}\n\nThe `isJson` method determines if a given string is valid JSON:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$result = Str::of('[1,2,3]')->isJson();\n\n// true\n\n$result = Str::of('{\"first\": \"John\", \"last\": \"Doe\"}')->isJson();\n\n// true\n\n$result = Str::of('{first: \"John\", last: \"Doe\"}')->isJson();\n\n// false\n```\n\n---\n\n## Eloquent Relationships Inserting and Updating Related Models\nEloquent Relationships | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-relationships.md\n\n\n## Inserting and Updating Related Models\n\n---\n\n## Eloquent Inserting and Updating Models Mass Assignment Mass Assignment and JSON Columns\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n#### Mass Assignment and JSON Columns\n\nWhen assigning JSON columns, each column's mass assignable key must be specified in your model's `$fillable` array. For security, Laravel does not support updating nested JSON attributes when using the `guarded` property:\n\n```php\n/**\n * The attributes that are mass assignable.\n *\n * @var array<int, string>\n */\nprotected $fillable = [\n    'options->enabled',\n];\n```\n\n---\n\n## Strings Strings `Str::isJson()` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `Str::isJson()` {.collection-method}\n\nThe `Str::isJson` method determines if the given string is valid JSON:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$result = Str::isJson('[1,2,3]');\n\n// true\n\n$result = Str::isJson('{\"first\": \"John\", \"last\": \"Doe\"}');\n\n// true\n\n$result = Str::isJson('{first: \"John\", last: \"Doe\"}');\n\n// false\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Updating Dependencies\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n### Updating Dependencies\n\n**Likelihood Of Impact: High**\n\nYou should update the following dependencies in your application's `composer.json` file:\n\n
\n\n- `laravel/framework` to `^12.0`\n- `phpunit/phpunit` to `^11.0`\n- `pestphp/pest` to `^3.0`\n\n
\n\n---\n\n## Http Tests Available Assertions Response Assertions assertJsonMissingValidationErrors\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertJsonMissingValidationErrors\n\nAssert that the response has no JSON validation errors for the given keys:\n\n```php\n$response->assertJsonMissingValidationErrors($keys);\n```\n\n> [!NOTE]\n> The more generic [assertValid](#assert-valid) method may be used to assert that a response does not have validation errors that were returned as JSON **and** that no errors were flashed to session storage.\n\n---\n\n## Scout Third-Party Engine Indexing Updating Records Conditionally Updating the Search Index\nScout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md\n\n\n#### Conditionally Updating the Search Index\n\nBy default, Scout will reindex an updated model regardless of which attributes were modified. If you would like to customize this behavior, you may define a `searchIndexShouldBeUpdated` method on your model:\n\n```php\n/**\n * Determine if the search index should be updated.\n */\npublic function searchIndexShouldBeUpdated(): bool\n{\n    return $this->wasRecentlyCreated || $this->wasChanged(['title', 'body']);\n}\n```\n\n_[Truncated]_"}]


cmd: rg -n "settings|exports|export" specs/02-API-ROUTES.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/05-BUSINESS-LOGIC.md


cmd: sed -n '1,260p' app/Models/StoreSettings.php


cmd: sed -n '1,280p' app/Livewire/Admin/Settings/Index.php


cmd: sed -n '1,240p' app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php


cmd: rg -n "StoreSettings|settings_json|bank_transfer_cancel_days|checkout|notification" app database tests specs/progress.md


command: sed -n '1,240p' app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php

TaxSettingsController.php

type: read

aggregated_output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Enums\TaxMode;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\TaxSettingsResource;
use App\Models\Store;
use App\Models\TaxSettings;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class TaxSettingsController extends Controller
{
    public function show(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        return TaxSettingsResource::make($this->settings($store));
    }

    public function update(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'mode' => ['required', Rule::in(array_map(fn (TaxMode $mode): string => $mode->value, TaxMode::cases()))],
            'provider' => ['required_if:mode,provider', Rule::in(['none', 'stripe_tax'])],
            'prices_include_tax' => ['required', 'boolean'],
            'config_json' => ['required', 'array'],
            'config_json.default_tax_rate' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.default_rate_bps' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates' => ['nullable', 'array'],
            'config_json.tax_rates.*.country_code' => ['required_with:config_json.tax_rates', 'string', 'size:2'],
            'config_json.tax_rates.*.rate' => ['required_with:config_json.tax_rates', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates.*.name' => ['required_with:config_json.tax_rates', 'string', 'max:50'],
            'config_json.tax_rates.*.shipping_taxed' => ['nullable', 'boolean'],
            'config_json.rates' => ['nullable', 'array'],
            'config_json.rates.*.country' => ['required_with:config_json.rates', 'string', 'size:2'],
            'config_json.rates.*.rate_bps' => ['required_with:config_json.rates', 'integer', 'min:0', 'max:10000'],
            'config_json.rates.*.name' => ['required_with:config_json.rates', 'string', 'max:50'],
        ]);

        $settings = TaxSettings::withoutGlobalScopes()->updateOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::from($validated['mode']),
                'provider' => $validated['mode'] === TaxMode::Provider->value ? $validated['provider'] : 'none',
                'prices_include_tax' => (bool) $validated['prices_include_tax'],
                'config_json' => $this->normalizeConfig($request->input('config_json', [])),
            ],
        );

        return TaxSettingsResource::make($settings->refresh());
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function settings(Store $store): TaxSettings
    {
        return TaxSettings::withoutGlobalScopes()->firstOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::Manual,
                'provider' => 'none',
                'prices_include_tax' => false,
                'config_json' => [
                    'name' => 'Tax',
                    'default_rate_bps' => 0,
                    'shipping_taxable' => true,
                    'rates' => [],
                ],
            ],
        );
    }

    /**
     * @param  array<string, mixed>  $config
     * @return array<string, mixed>
     */
    private function normalizeConfig(array $config): array
    {
        $rates = collect($config['tax_rates'] ?? $config['rates'] ?? [])
            ->map(fn (array $rate): array => [
                'country' => strtoupper((string) ($rate['country_code'] ?? $rate['country'])),
                'rate_bps' => (int) ($rate['rate'] ?? $rate['rate_bps']),
                'name' => (string) $rate['name'],
                'shipping_taxed' => (bool) ($rate['shipping_taxed'] ?? true),
            ])
            ->values()
            ->all();

        return [
            ...$config,
            'default_rate_bps' => (int) ($config['default_tax_rate'] ?? $config['default_rate_bps'] ?? 0),
            'shipping_taxable' => (bool) ($config['shipping_taxable'] ?? true),
            'rates' => $rates,
        ];
    }
}


command: sed -n '1,280p' app/Livewire/Admin/Settings/Index.php

aggregated_output:
<?php

namespace App\Livewire\Admin\Settings;

use App\Enums\StoreDomainType;
use App\Models\Store;
use App\Models\StoreDomain;
use App\Models\StoreSettings;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $storeName = '';

    public string $storeHandle = '';

    public string $defaultCurrency = 'EUR';

    public string $defaultLocale = 'en';

    public string $timezone = 'UTC';

    public bool $announcementEnabled = false;

    public string $announcementText = '';

    public bool $guestCheckoutEnabled = true;

    public string $newHostname = '';

    public string $newType = 'storefront';

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);

        $settings = $this->storeSettings($store)->settings_json ?? [];

        $this->storeId = $store->getKey();
        $this->storeName = $store->name;
        $this->storeHandle = $store->handle;
        $this->defaultCurrency = $store->default_currency;
        $this->defaultLocale = $store->default_locale;
        $this->timezone = $store->timezone;
        $this->announcementEnabled = (bool) data_get($settings, 'announcement.enabled', false);
        $this->announcementText = (string) data_get($settings, 'announcement.text', '');
        $this->guestCheckoutEnabled = (bool) data_get($settings, 'checkout.guest_checkout_enabled', true);
    }

    public function save(): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $validated = $this->validate([
            'storeName' => ['required', 'string', 'max:255'],
            'defaultCurrency' => ['required', Rule::in($this->currencyOptions())],
            'defaultLocale' => ['required', Rule::in(array_keys($this->localeOptions()))],
            'timezone' => ['required', Rule::in(timezone_identifiers_list())],
            'announcementEnabled' => ['boolean'],
            'announcementText' => ['nullable', 'string', 'max:255'],
            'guestCheckoutEnabled' => ['boolean'],
        ]);

        $store->forceFill([
            'name' => $validated['storeName'],
            'default_currency' => $validated['defaultCurrency'],
            'default_locale' => $validated['defaultLocale'],
            'timezone' => $validated['timezone'],
        ])->save();

        $settings = $this->storeSettings($store);
        $settings->forceFill([
            'settings_json' => array_replace_recursive($settings->settings_json ?? [], [
                'announcement' => [
                    'enabled' => $this->announcementEnabled,
                    'text' => $this->announcementText,
                ],
                'checkout' => [
                    'guest_checkout_enabled' => $this->guestCheckoutEnabled,
                ],
            ]),
            'updated_at' => now(),
        ])->save();

        session()->flash('status', 'Settings saved');
        $this->dispatch('toast', type: 'success', message: __('Settings saved'));
    }

    public function addDomain(): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $validated = $this->validate([
            'newHostname' => [
                'required',
                'string',
                'max:255',
                'regex:/^[a-z0-9.-]+$/i',
                Rule::unique('store_domains', 'hostname'),
            ],
            'newType' => ['required', Rule::in(array_column(StoreDomainType::cases(), 'value'))],
        ], [], [
            'newHostname' => 'hostname',
            'newType' => 'domain type',
        ]);

        StoreDomain::query()->create([
            'store_id' => $store->getKey(),
            'hostname' => mb_strtolower(trim($validated['newHostname'])),
            'type' => StoreDomainType::from($validated['newType']),
            'is_primary' => $this->domains()->isEmpty(),
            'tls_mode' => 'managed',
        ]);

        $this->reset('newHostname');

        session()->flash('status', 'Domain added');
        $this->dispatch('toast', type: 'success', message: __('Domain added'));
    }

    public function setPrimary(int $domainId): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $domain = $this->domain($domainId);

        StoreDomain::query()
            ->where('store_id', $store->getKey())
            ->update(['is_primary' => false]);

        $domain->forceFill(['is_primary' => true])->save();

        session()->flash('status', 'Primary domain updated');
    }

    public function removeDomain(int $domainId): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $domain = $this->domain($domainId);
        $wasPrimary = $domain->is_primary;

        $domain->delete();

        if ($wasPrimary) {
            StoreDomain::query()
                ->where('store_id', $store->getKey())
                ->orderBy('id')
                ->first()
                ?->forceFill(['is_primary' => true])
                ->save();
        }

        session()->flash('status', 'Domain removed');
    }

    /**
     * @return Collection<int, StoreDomain>
     */
    public function domains(): Collection
    {
        return StoreDomain::query()
            ->where('store_id', $this->storeId)
            ->orderByDesc('is_primary')
            ->orderBy('hostname')
            ->get();
    }

    /**
     * @return array<string, string>
     */
    public function localeOptions(): array
    {
        return [
            'en' => 'English',
            'de' => 'German',
            'fr' => 'French',
        ];
    }

    /**
     * @return list<string>
     */
    public function currencyOptions(): array
    {
        return ['EUR', 'USD', 'GBP', 'CHF'];
    }

    /**
     * @return list<string>
     */
    public function timezoneOptions(): array
    {
        return collect(timezone_identifiers_list())
            ->filter(fn (string $timezone): bool => str_starts_with($timezone, 'Europe/') || str_starts_with($timezone, 'America/'))
            ->values()
            ->all();
    }

    public function render(): mixed
    {
        return view('livewire.admin.settings.index', [
            'domains' => $this->domains(),
            'currencyOptions' => $this->currencyOptions(),
            'localeOptions' => $this->localeOptions(),
            'timezoneOptions' => $this->timezoneOptions(),
        ])->layout('layouts.app', [
            'title' => __('Settings'),
        ]);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->whereKey($this->storeId)->firstOrFail();
    }

    private function storeSettings(Store $store): StoreSettings
    {
        return StoreSettings::query()->firstOrCreate(
            ['store_id' => $store->getKey()],
            ['settings_json' => []],
        );
    }

    private function domain(int $domainId): StoreDomain
    {
        return StoreDomain::query()
            ->where('store_id', $this->storeId)
            ->whereKey($domainId)
            ->firstOrFail();
    }
}


command: sed -n '1,260p' app/Models/StoreSettings.php

aggregated_output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class StoreSettings extends Model
{
    /** @use HasFactory<\Database\Factories\StoreSettingsFactory> */
    use HasFactory;

    public $incrementing = false;

    public const CREATED_AT = null;

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'settings_json',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'settings_json' => 'array',
        ];
    }
}


command: rg -n "settings|exports|export" specs/02-API-ROUTES.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/05-BUSINESS-LOGIC.md

type: search

cmd: rg -n 'settings|exports|export' specs/02-API-ROUTES.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/05-BUSINESS-LOGIC.md

query: settings|exports|export

aggregated_output:
specs/02-API-ROUTES.md:92:| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
specs/02-API-ROUTES.md:93:| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
specs/02-API-ROUTES.md:94:| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |
specs/02-API-ROUTES.md:101:| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |
specs/02-API-ROUTES.md:128:| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |
specs/02-API-ROUTES.md:136:| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |
specs/02-API-ROUTES.md:142:| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |
specs/02-API-ROUTES.md:1379:      "read-settings",
specs/02-API-ROUTES.md:1380:      "write-settings",
specs/02-API-ROUTES.md:2284:| GET | `/api/admin/v1/stores/{storeId}/shipping/zones` | `read-settings` | List shipping zones with rates | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2285:| POST | `/api/admin/v1/stores/{storeId}/shipping/zones` | `write-settings` | Create a shipping zone | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2286:| PUT | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}` | `write-settings` | Update a shipping zone | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2287:| POST | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}/rates` | `write-settings` | Add a shipping rate to a zone | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2420:| GET | `/api/admin/v1/stores/{storeId}/tax/settings` | `read-settings` | Get tax configuration | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2421:| PUT | `/api/admin/v1/stores/{storeId}/tax/settings` | `write-settings` | Update tax configuration | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2423:#### GET /api/admin/v1/stores/{storeId}/tax/settings
specs/02-API-ROUTES.md:2460:#### PUT /api/admin/v1/stores/{storeId}/tax/settings
specs/02-API-ROUTES.md:2472:    "stripe_tax_settings_id": "txr_abc123"
specs/02-API-ROUTES.md:2500:| PUT | `/api/admin/v1/stores/{storeId}/themes/{themeId}/settings` | `write-themes` | Update theme customization settings | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2558:#### PUT /api/admin/v1/stores/{storeId}/themes/{themeId}/settings
specs/02-API-ROUTES.md:2560:Update theme settings (customization values).
specs/02-API-ROUTES.md:2566:  "settings_json": {
specs/02-API-ROUTES.md:2587:| `settings_json` | object | Yes | Must conform to the theme's settings schema. |
specs/02-API-ROUTES.md:2719:| POST | `/api/admin/v1/stores/{storeId}/search/reindex` | `write-settings` | Trigger full search reindex (async) | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2720:| GET | `/api/admin/v1/stores/{storeId}/search/status` | `read-settings` | Get current search index status | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2771:| POST | `/api/admin/v1/stores/{storeId}/exports/orders` | `read-orders` | Queue an order export job (async) | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2772:| GET | `/api/admin/v1/stores/{storeId}/exports/{exportId}` | `read-orders` | Check export status and get download URL | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2834:#### POST /api/admin/v1/stores/{storeId}/exports/orders
specs/02-API-ROUTES.md:2836:Queue an order export job. The export runs asynchronously and produces a downloadable CSV.
specs/02-API-ROUTES.md:2860:  "export_id": 1,
specs/02-API-ROUTES.md:2868:#### GET /api/admin/v1/stores/{storeId}/exports/{exportId}
specs/02-API-ROUTES.md:2870:Check export status and get download URL.
specs/03-ADMIN-UI.md:96:Settings                         -> admin.settings.index
specs/03-ADMIN-UI.md:148:    - Settings (links to /admin/settings)
specs/03-ADMIN-UI.md:1382:**Route:** `GET /admin/settings`
specs/03-ADMIN-UI.md:1386:**Blade view:** `resources/views/livewire/admin/settings/general.blade.php`
specs/03-ADMIN-UI.md:1400:| `save()` | Validates and saves store settings |
specs/03-ADMIN-UI.md:1413:- Left: Heading "Defaults" + description "Currency, language, and timezone settings."
specs/03-ADMIN-UI.md:1423:Domains is a tab within the Settings page (`/admin/settings`), not a separate route. The Settings page has tabs: General, Domains, Shipping, Taxes, Checkout, Notifications.
specs/03-ADMIN-UI.md:1427:**Blade view:** `resources/views/livewire/admin/settings/domains.blade.php`
specs/03-ADMIN-UI.md:1473:**Route:** `GET /admin/settings/shipping`
specs/03-ADMIN-UI.md:1477:**Blade view:** `resources/views/livewire/admin/settings/shipping.blade.php`
specs/03-ADMIN-UI.md:1590:**Route:** `GET /admin/settings/taxes`
specs/03-ADMIN-UI.md:1594:**Blade view:** `resources/views/livewire/admin/settings/taxes.…1294 chars truncated…nym and stop word settings |
specs/03-ADMIN-UI.md:2416:| `App\Livewire\Admin\Settings\General` | `livewire.admin.settings.general` | `GET /admin/settings` |
specs/03-ADMIN-UI.md:2417:| `App\Livewire\Admin\Settings\Domains` | `livewire.admin.settings.domains` | Tab within `GET /admin/settings` |
specs/03-ADMIN-UI.md:2418:| `App\Livewire\Admin\Settings\Shipping` | `livewire.admin.settings.shipping` | `GET /admin/settings/shipping` |
specs/03-ADMIN-UI.md:2419:| `App\Livewire\Admin\Settings\Taxes` | `livewire.admin.settings.taxes` | `GET /admin/settings/taxes` |
specs/03-ADMIN-UI.md:2428:| `App\Livewire\Admin\Search\Settings` | `livewire.admin.search.settings` | `GET /admin/search/settings` |
specs/05-BUSINESS-LOGIC.md:41:3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.
specs/05-BUSINESS-LOGIC.md:90:| Manage store settings | Yes | Yes | No | No |
specs/05-BUSINESS-LOGIC.md:341:- Finds carts where status = `active` AND updated_at < (now - 14 days). The 14-day threshold is configurable via `store_settings`.
specs/05-BUSINESS-LOGIC.md:737:- The TaxCalculationRequest contains: line items (with discounted amounts), shipping amount, shipping address, store tax settings.
specs/05-BUSINESS-LOGIC.md:750:2. Use the tax rate defined in `tax_settings.config_json` for the matched zone, or the global default rate.
specs/05-BUSINESS-LOGIC.md:763:Controlled by `tax_settings.prices_include_tax` (boolean).
specs/05-BUSINESS-LOGIC.md:802:- Fallback behavior (configurable per store in `tax_settings.config_json`):
specs/05-BUSINESS-LOGIC.md:1047:- Configurable timeout: `store_settings.settings_json` key `bank_transfer_cancel_days`, default = 7.
specs/05-BUSINESS-LOGIC.md:1082:- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
specs/05-BUSINESS-LOGIC.md:1281:`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.
specs/05-BUSINESS-LOGIC.md:1285:All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.
specs/01-DATABASE-SCHEMA.md:139:### store_settings
specs/01-DATABASE-SCHEMA.md:141:Single-row-per-store JSON settings bag.
specs/01-DATABASE-SCHEMA.md:146:| settings_json | TEXT | No | '{}' | - | JSON settings object |
specs/01-DATABASE-SCHEMA.md:411:### theme_settings
specs/01-DATABASE-SCHEMA.md:418:| settings_json | TEXT | No | '{}' | - | JSON settings object |
specs/01-DATABASE-SCHEMA.md:492:### search_settings
specs/01-DATABASE-SCHEMA.md:658:### tax_settings
specs/01-DATABASE-SCHEMA.md:1212:    stores ||--|| store_settings : "has"
specs/01-DATABASE-SCHEMA.md:1245:    store_settings {
specs/01-DATABASE-SCHEMA.md:1247:        TEXT settings_json
specs/01-DATABASE-SCHEMA.md:1324:    themes ||--|| theme_settings : "configured by"
specs/01-DATABASE-SCHEMA.md:1341:    theme_settings {
specs/01-DATABASE-SCHEMA.md:1343:        TEXT settings_json
specs/01-DATABASE-SCHEMA.md:1484:The application requires specific SQLite PRAGMA settings configured in the Laravel database configuration file (`config/database.php`). These settings must be applied for correct and performant operation.
specs/01-DATABASE-SCHEMA.md:1526:        store_settings
specs/01-DATABASE-SCHEMA.md:1531:        search_settings
specs/01-DATABASE-SCHEMA.md:1533:        tax_settings
specs/01-DATABASE-SCHEMA.md:1550:        theme_settings
specs/01-DATABASE-SCHEMA.md:1586:    stores --> store_settings
specs/01-DATABASE-SCHEMA.md:1591:    stores --> search_settings
specs/01-DATABASE-SCHEMA.md:1593:    stores --> tax_settings
specs/01-DATABASE-SCHEMA.md:1608:    themes --> theme_settings
specs/01-DATABASE-SCHEMA.md:1645:- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1673:| 1 - Foundation and Multi-Store Tenancy | organizations, stores, store_domains, users, store_users, store_settings | 6 |
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
specs/01-DATABASE-SCHEMA.md:1676:| 4 - Search | search_settings, search_queries | 2 |
specs/01-DATABASE-SCHEMA.md:1677:| 5 - Cart, Checkout, Discounts, Shipping, Taxes | carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts | 7 |