750	#### Step 6.1: Customer Auth
751
752	- `Customer` model with custom `customer` guard (already configured in Phase 1)
753	- Custom `CustomerUserProvider` - scopes queries by `store_id`
754	- Customer email unique per store, not globally
755
756	#### Step 6.2: Customer Account Pages (Livewire)
757
758	**Files to create:**
759
760	| Component | Route | Description |
761	|-----------|-------|-------------|
762	| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
763	| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
764	| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
765	| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
766	| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
767	| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |
768
769	Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
770
771	---
772
773	### Phase 7: Admin Panel
774
775	**Priority:** MEDIUM - merchant management interface.
776
777	#### Step 7.1: Admin Layout
778
779	**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
780
781	**Components to create:**
782
783	| Component | Description |
784	|-----------|-------------|
785	| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
786	| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
787	| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |
788
789	Toast notifications via Livewire events.
790
791	#### Step 7.2: Dashboard
792
793	**File to create:** `Admin\Dashboard` Livewire component
794
795	Requirements:
796	- KPI tiles: total sales (period), order count, average order value, conversion rate
797	- Orders chart (last 30 days line chart)
798	- Recent orders table (last 10)
799	- Date range filtering via Livewire properties
800
801	#### Step 7.3: Product Management
802
803	**Files to create:**
804
805	| Component | Description |
806	|-----------|-------------|
807	| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
808	| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |
809
810	> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.
811
812	Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.
813
814	Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
815
816	#### Step 7.4: Order Management
817
818	**Files to create:**
819
820	| Component | Description |
821	|-----------|-------------|
822	| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
823	| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |
824
825	Fulfillment modal: select lines and quantities, add tracking info, submit.
826	Refund modal: enter amount (partial or full), reason, restock checkbox, submit.
827
828	#### Step 7.5: Other Admin Sections
829
830	**Files to create:**
831
832	| Component | Description |
833	|-----------|-------------|
834	| `Admin\Collections\Index` | Collection list |
835	| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
836	| `Admin\Customers\Index` | Customer list with search |
837	| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
838	| `Admin\Discounts\Index` | Discount list with status/type filters |
839	| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
840	| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
841	| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
842	| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
843	| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
844	| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
845	| `Admin\Pages\Index` | Content pages list |
846	| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
847	| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
848	| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
849	| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
850	| `Admin\Apps\Index` | App directory |
851	| `Admin\Apps\Show` | Installed app detail |
852	| `Admin\Developers\Index` | API token management, webhook subscriptions |
853
854	---
855
856	### Phase 8: Search
857
858	**Priority:** LOW - enhances product discovery.
859
860	#### Step 8.1: Migrations
861
862	1. `create_search_settings_table` - PK: `store_id`
863	2. `create_search_queries_table` - FK: `store_id`
864	3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table
865
866	#### Step 8.2: Search Service
867
868	**File to create:** `App\Services\SearchService`
869
870	| Method | Description |
871	|--------|-------------|
872	| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
873	| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
874	| `syncProduct(Product $product): void` | Upsert into FTS5 index |
875	| `removeProduct(int $productId): void` | Delete from FTS5 index |
876
877	**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.
878
879	#### Step 8.3: Search UI
880
881	**Files to create:**
882
883	| Component | Description |
884	|-----------|-------------|
885	| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
886	| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |
887
888	---
889
890	### Phase 9: Analytics
891
892	**Priority:** LOW - reporting and insights.
893
894	#### Step 9.1: Migrations
895
896	1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
897	2. `create_analytics_daily_table` - composite PK: `store_id` + `date`
898
899	#### Step 9.2: Analytics Service
900
901	**File to create:** `App\Services\AnalyticsService`
902
903	| Method | Description |
904	|--------|-------------|
905	| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
906	| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |
907
908	Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.
909
910	**File to create:** `App\Jobs\AggregateAnalytics`
911
912	Requirements:
913	- Runs daily via `routes/console.php`
914	- Aggregates raw events into `analytics_daily` rows
915	- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
916
917	---
918
919	### Phase 10: Apps and Webhooks
920
921	**Priority:** LOW - extensibility.
922
923	#### Step 10.1: Migrations
924
925	1. `create_apps_table`
926	2. `create_app_installations_table` - FK: `store_id`, `app_id`
927	3. `create_oauth_clients_table` - FK: `app_id`
928	4. `create_oauth_tokens_table` - FK: `installation_id`
929	5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
930	6. `create_webhook_deliveries_table` - FK: `subscription_id`
931
932	#### Step 10.2: Webhook Service
933
934	**File to create:** `App\Services\WebhookService`
935
936	| Method | Description |
937	|--------|-------------|
938	| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
939	| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
940	| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |
941
942	**File to create:** `App\Jobs\DeliverWebhook`
943
944	Requirements:
945	- HTTP POST to target URL with JSON payload
946	- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
947	- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
948	- Records response in `webhook_deliveries`
949	- Circuit breaker: after 5 consecutive failures, pause subscription
950
951	---
952
953	### Phase 11: Polish
954
955	**Priority:** LOW but important for completeness.
956
957	1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
958	2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
959	3. Dark mode: `dark:` prefix on all storefront views and admin views
960	4. Error pages: styled 404 and 503 pages matching the storefront theme
961	5. Structured logging: JSON channel in `config/logging.php`
962	6. Seed data: comprehensive seeders for demo stores
963
964	---
965
966	### Phase 12: Full Test Suite Execution
967
968	**Priority:** Runs after all phases are complete.
969
970	**Verification steps:**
971
972	1. Run the full unit and feature test suite and confirm all tests pass
973	2. Run all browser tests and confirm they pass
974	3. Run the code style formatter/checker and confirm conformance
975	4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
976	5. Manual smoke: visit storefront, navigate products, add to cart, checkout
977	6. Manual smoke: visit admin login, authenticate, manage products and orders
978
979	---
980
981	## 3. Pest Unit/Feature Test Specification
982
983	All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.
984
985	### Test Directory Structure
986
987	| Directory | Files |
988	|-----------|-------|
989	| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
990	| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
991	| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
992	| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
993	| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
994	| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
995	| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
996	| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
997	| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
998	| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
999	| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |
1000	| `tests/Feature/Analytics/` | `EventIngestionTest.php`, `AggregationTest.php` |
1001	| `tests/Feature/Webhooks/` | `WebhookDeliveryTest.php`, `WebhookSignatureTest.php` |
1002	| `tests/Feature/Api/` | `StorefrontCartApiTest.php`, `StorefrontCheckoutApiTest.php`, `AdminProductApiTest.php`, `AdminOrderApiTest.php` |
1003	| `tests/Browser/` | Covered in `08-PLAYWRIGHT-E2E-PLAN.md` |
1004
1005	---
1006
1007	### Unit Tests
1008
1009	---
1010
1011	#### tests/Unit/PricingEngineTest.php
1012
1013	**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.
1014
1015	| Test Name | Setup / Preconditions | Key Assertions | Category |
1016	|-----------|----------------------|----------------|----------|
1017	| calculates subtotal from line items | 2 lines: variant A 2499 cents x 2, variant B 7999 cents x 1 | subtotal = 12997 | happy path |
1018	| calculates subtotal for a single line | 1 line: 1500 cents x 3 | subtotal = 4500 | happy path |
1019	| returns zero subtotal for empty cart | 0 lines | subtotal = 0 | edge case |
1020	| applies percent discount correctly | subtotal 10000, discount 10% (value_amount = 10, value_type = percent) | discount = 1000, discounted subtotal = 9000 | happy path |
1021	| applies fixed discount correctly | subtotal 10000, discount 500 cents fixed | discount = 500, discounted subtotal = 9500 | happy path |
1022	| caps fixed discount at subtotal so it never goes negative | subtotal 300, discount 500 cents fixed | discount = 300, discounted subtotal = 0 | edge case |
1023	| applies free shipping discount by zeroing shipping | subtotal 5000, shipping 499, discount type free_shipping | shipping = 0 | happy path |
1024	| calculates tax exclusive correctly | net amount 10000, rate 1900 basis points (19%) | tax = 1900, gross = 11900 | happy path |
1025	| extracts tax from inclusive price correctly | gross amount 11900, rate 1900 basis points (19%) | tax = 1900, net = 10000 | happy path |
1026	| returns zero tax when rate is zero | amount 10000, rate 0 | tax = 0 | edge case |
1027	| calculates shipping flat rate | rate config type=flat, amount=499 | shipping = 499 | happy path |
1028	| calculates full checkout totals end to end | 2 lines x 2499 = 4998, discount WELCOME10 (10%), shipping flat 499, tax 19% inclusive; pipeline: subtotal 4998 -> discount 499 -> discounted 4499 -> shipping 499 -> tax extracted from gross | total calculation matches expected value | happy path |
1029	| handles rounding correctly with odd cent amounts | 3 lines with prices that create fractional discount allocations | sum of rounded allocations equals total discount (no off-by-one) | edge case |
1030	| produces identical results for identical inputs | Run calculation twice with same inputs | both PricingResult objects are identical | edge case |
1031
1032	---
1033
1034	#### tests/Unit/DiscountCalculatorTest.php
1035
1036	**Setup:** Instantiate `DiscountService` with mock store and cart.
1037
1038	| Test Name | Setup / Preconditions | Key Assertions | Category |
1039	|-----------|----------------------|----------------|----------|
1040	| validates an active discount code | discount: status=active, starts_at=yesterday, ends_at=tomorrow | validation passes, returns Discount model | happy path |
1041	| rejects an expired discount code | discount: status=active, ends_at=yesterday | throws InvalidDiscountException with reason 'expired' | validation |
1042	| rejects a not-yet-active discount code | discount: status=active, starts_at=tomorrow | throws InvalidDiscountException with reason 'not_yet_active' | validation |
1043	| rejects a discount that has reached its usage limit | discount: usage_limit=10, usage_count=10 | throws InvalidDiscountException with reason 'usage_limit_reached' | validation |
1044	| rejects an unknown discount code | look up code 'DOESNOTEXIST' | throws InvalidDiscountException with reason 'not_found' | validation |
1045	| performs case-insensitive code lookup | discount with code 'SUMMER20'; validate code 'summer20' | validation passes | edge case |
1046	| enforces minimum purchase amount rule | discount with rules_json containing minimum_purchase: 5000; cart subtotal: 3000 | throws InvalidDiscountException with reason 'minimum_not_met' | validation |
1047	| passes minimum purchase when cart meets threshold | discount with rules_json minimum_purchase: 5000; cart subtotal: 5000 | validation passes | happy path |
1048	| calculates percent discount amount | discount: 15%, subtotal: 10000 | discount amount = 1500 | happy path |
1049	| calculates fixed discount amount | discount: 500 cents fixed, subtotal: 10000 | discount amount = 500 | happy path |
1050	| handles free shipping discount type | discount: type=free_shipping | discount amount = 0, free_shipping flag = true | happy path |
1051	| allocates discount proportionally across multiple lines | 2 lines: line A subtotal 7500, line B subtotal 2500 (total 10000); discount: 10% = 1000 | line A allocation = 750, line B allocation = 250 | happy path |
1052	| distributes rounding remainder to the last qualifying line | 3 lines with subtotals that create uneven division | sum of allocations exactly equals total discount | edge case |
1053
1054	---
1055
1056	#### tests/Unit/TaxCalculatorTest.php
1057
1058	**Setup:** Instantiate `TaxCalculator` directly.
1059
1060	| Test Name | Setup / Preconditions | Key Assertions | Category |
1061	|-----------|----------------------|----------------|----------|
1062	| calculates manual tax exclusive | net amount: 10000, rate: 1900 (19%) | tax = 1900 | happy path |
1063	| extracts manual tax from inclusive amount | gross amount: 11900, rate: 1900 (19%) | tax = 1900, net = 10000 | happy path |
1064	| returns zero tax when no rate is configured | amount: 10000, rate: 0 | tax = 0 | edge case |
1065	| handles zero amount lines | amount: 0, rate: 1900 | tax = 0 | edge case |
1066	| calculates tax with non-standard rate | net amount: 8999, rate: 700 (7%) | tax = 629 (rounded) | happy path |
1067	| extracts tax correctly for small amounts | gross amount: 119, rate: 1900 | tax = 19, net = 100 | edge case |
1068	| handles high tax rates | net amount: 10000, rate: 2500 (25%) | tax = 2500 | happy path |
1069
1070	---
1071
1072	#### tests/Unit/ShippingCalculatorTest.php
1073
1074	**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.
1075
1076	| Test Name | Setup / Preconditions | Key Assertions | Category |
1077	|-----------|----------------------|----------------|----------|
1078	| matches a zone by country code | zone countries_json: ["DE","AT","CH"]; address country: "DE" | zone matched | happy path |
1079	| matches a zone by region code | zone regions_json: ["US-NY","US-CA"]; address: country "US", province_code "US-NY" | zone matched | happy path |
1080	| returns empty when no zone matches the address | zone countries_json: ["DE"]; address country: "FR" | empty collection returned | edge case |
1081	| calculates a flat rate | rate type: flat, config: amount 499 | shipping = 499 | happy path |
1082	| calculates a weight-based rate | rate type: weight, config: ranges 0-500g=499, 501-2000g=899; cart total weight: 750g | shipping = 899 | happy path |
1083	| calculates a price-based rate | rate type: price, config: ranges 0-5000=799, 5001-999999=399; cart subtotal: 7500 | shipping = 399 | happy path |
1084	| returns zero shipping when no items require shipping | all cart line variants have requires_shipping = false | shipping = 0 | edge case |
1085	| returns the correct rate when multiple zones match and the first is selected | two zones match, each with different rates | returns rates from both zones for customer selection | edge case |
1086	| skips inactive rates | rate with is_active = 0 | rate not included in available rates | edge case |
1087
1088	---
1089
1090	#### tests/Unit/CartVersionTest.php
1091
1092	**Setup:** Instantiate Cart model or CartService.
1093
1094	| Test Name | Setup / Preconditions | Key Assertions | Category |
1095	|-----------|----------------------|----------------|----------|
1096	| starts at version 1 | create new cart | cart_version = 1 | happy path |
1097	| increments version on add line | add a line to cart | cart_version = 2 | happy path |
1098	| increments version on update quantity | update line quantity | cart_version incremented by 1 | happy path |
1099	| increments version on remove line | remove a line | cart_version incremented by 1 | happy path |

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |
|-----------|-------|-------------|
| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |

Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.

---

### Phase 7: Admin Panel

**Priority:** MEDIUM - merchant management interface.

#### Step 7.1: Admin Layout

**File to create:** `resources/views/livewire/admin/layout/app.blade.php`

**Components to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |

Toast notifications via Livewire events.

#### Step 7.2: Dashboard

**File to create:** `Admin\Dashboard` Livewire component

Requirements:
- KPI tiles: total sales (period), order count, average order value, conversion rate
- Orders chart (last 30 days line chart)
- Recent orders table (last 10)
- Date range filtering via Livewire properties

#### Step 7.3: Product Management

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |

> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.

Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.

Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.

#### Step 7.4: Order Management

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |

Fulfillment modal: select lines and quantities, add tracking info, submit.
Refund modal: enter amount (partial or full), reason, restock checkbox, submit.

#### Step 7.5: Other Admin Sections

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Collections\Index` | Collection list |
| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
| `Admin\Customers\Index` | Customer list with search |
| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
| `Admin\Discounts\Index` | Discount list with status/type filters |
| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
| `Admin\Pages\Index` | Content pages list |
| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
| `Admin\Apps\Index` | App directory |
| `Admin\Apps\Show` | Installed app detail |
| `Admin\Developers\Index` | API token management, webhook subscriptions |

---

### Phase 8: Search

**Priority:** LOW - enhances product discovery.

#### Step 8.1: Migrations

1. `create_search_settings_table` - PK: `store_id`
2. `create_search_queries_table` - FK: `store_id`
3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table

#### Step 8.2: Search Service

**File to create:** `App\Services\SearchService`

| Method | Description |
|--------|-------------|
| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
| `syncProduct(Product $product): void` | Upsert into FTS5 index |
| `removeProduct(int $productId): void` | Delete from FTS5 index |

**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.

#### Step 8.3: Search UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |

---

### Phase 9: Analytics

**Priority:** LOW - reporting and insights.

#### Step 9.1: Migrations

1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

**File to create:** `App\Services\AnalyticsService`

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

**File to create:** `App\Jobs\AggregateAnalytics`

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

**File to create:** `App\Services\WebhookService`

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |

**File to create:** `App\Jobs\DeliverWebhook`

Requirements:
- HTTP POST to target URL with JSON payload
- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
- Records response in `webhook_deliveries`
- Circuit breaker: after 5 consecutive failures, pause subscription

---

### Phase 11: Polish

**Priority:** LOW but important for completeness.

1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
3. Dark mode: `dark:` prefix on all storefront views and admin views
4. Error pages: styled 404 and 503 pages matching the storefront theme
5. Structured logging: JSON channel in `config/logging.php`
6. Seed data: comprehensive seeders for demo stores

---

### Phase 12: Full Test Suite Execution

**Priority:** Runs after all phases are complete.

**Verification steps:**

1. Run the full unit and feature test suite and confirm all tests pass
2. Run all browser tests and confirm they pass
3. Run the code style formatter/checker and confirm conformance
4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
5. Manual smoke: visit storefront, navigate products, add to cart, checkout
6. Manual smoke: visit admin login, authenticate, manage products and orders

---

## 3. Pest Unit/Feature Test Specification

All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.

### Test Directory Structure

| Directory | Files |
|-----------|-------|
| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |
| `tests/Feature/Analytics/` | `EventIngestionTest.php`, `AggregationTest.php` |
| `tests/Feature/Webhooks/` | `WebhookDeliveryTest.php`, `WebhookSignatureTest.php` |
| `tests/Feature/Api/` | `StorefrontCartApiTest.php`, `StorefrontCheckoutApiTest.php`, `AdminProductApiTest.php`, `AdminOrderApiTest.php` |
| `tests/Browser/` | Covered in `08-PLAYWRIGHT-E2E-PLAN.md` |

---

### Unit Tests

---

#### tests/Unit/PricingEngineTest.php

**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates subtotal from line items | 2 lines: variant A 2499 cents x 2, variant B 7999 cents x 1 | subtotal = 12997 | happy path |
| calculates subtotal for a single line | 1 line: 1500 cents x 3 | subtotal = 4500 | happy path |
| returns zero subtotal for empty cart | 0 lines | subtotal = 0 | edge case |
| applies percent discount correctly | subtotal 10000, discount 10% (value_amount = 10, value_type = percent) | discount = 1000, discounted subtotal = 9000 | happy path |
| applies fixed discount correctly | subtotal 10000, discount 500 cents fixed | discount = 500, discounted subtotal = 9500 | happy path |
| caps fixed discount at subtotal so it never goes negative | subtotal 300, discount 500 cents fixed | discount = 300, discounted subtotal = 0 | edge case |
| applies free shipping discount by zeroing shipping | subtotal 5000, shipping 499, discount type free_shipping | shipping = 0 | happy path |
| calculates tax exclusive correctly | net amount 10000, rate 1900 basis points (19%) | tax = 1900, gross = 11900 | happy path |
| extracts tax from inclusive price correctly | gross amount 11900, rate 1900 basis points (19%) | tax = 1900, net = 10000 | happy path |
| returns zero tax when rate is zero | amount 10000, rate 0 | tax = 0 | edge case |
| calculates shipping flat rate | rate config type=flat, amount=499 | shipping = 499 | happy path |
| calculates full checkout totals end to end | 2 lines x 2499 = 4998, discount WELCOME10 (10%), shipping flat 499, tax 19% inclusive; pipeline: subtotal 4998 -> discount 499 -> discounted 4499 -> shipping 499 -> tax extracted from gross | total calculation matches expected value | happy path |
| handles rounding correctly with odd cent amounts | 3 lines with prices that create fractional discount allocations | sum of rounded allocations equals total discount (no off-by-one) | edge case |
| produces identical results for identical inputs | Run calculation twice with same inputs | both PricingResult objects are identical | edge case |

---

#### tests/Unit/DiscountCalculatorTest.php

**Setup:** Instantiate `DiscountService` with mock store and cart.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| validates an active discount code | discount: status=active, starts_at=yesterday, ends_at=tomorrow | validation passes, returns Discount model | happy path |
| rejects an expired discount code | discount: status=active, ends_at=yesterday | throws InvalidDiscountException with reason 'expired' | validation |
| rejects a not-yet-active discount code | discount: status=active, starts_at=tomorrow | throws InvalidDiscountException with reason 'not_yet_active' | validation |
| rejects a discount that has reached its usage limit | discount: usage_limit=10, usage_count=10 | throws InvalidDiscountException with reason 'usage_limit_reached' | validation |
| rejects an unknown discount code | look up code 'DOESNOTEXIST' | throws InvalidDiscountException with reason 'not_found' | validation |
| performs case-insensitive code lookup | discount with code 'SUMMER20'; validate code 'summer20' | validation passes | edge case |
| enforces minimum purchase amount rule | discount with rules_json containing minimum_purchase: 5000; cart subtotal: 3000 | throws InvalidDiscountException with reason 'minimum_not_met' | validation |
| passes minimum purchase when cart meets threshold | discount with rules_json minimum_purchase: 5000; cart subtotal: 5000 | validation passes | happy path |
| calculates percent discount amount | discount: 15%, subtotal: 10000 | discount amount = 1500 | happy path |
| calculates fixed discount amount | discount: 500 cents fixed, subtotal: 10000 | discount amount = 500 | happy path |
| handles free shipping discount type | discount: type=free_shipping | discount amount = 0, free_shipping flag = true | happy path |
| allocates discount proportionally across multiple lines | 2 lines: line A subtotal 7500, line B subtotal 2500 (total 10000); discount: 10% = 1000 | line A allocation = 750, line B allocation = 250 | happy path |
| distributes rounding remainder to the last qualifying line | 3 lines with subtotals that create uneven division | sum of allocations exactly equals total discount | edge case |

---

#### tests/Unit/TaxCalculatorTest.php

**Setup:** Instantiate `TaxCalculator` directly.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates manual tax exclusive | net amount: 10000, rate: 1900 (19%) | tax = 1900 | happy path |
| extracts manual tax from inclusive amount | gross amount: 11900, rate: 1900 (19%) | tax = 1900, net = 10000 | happy path |
| returns zero tax when no rate is configured | amount: 10000, rate: 0 | tax = 0 | edge case |
| handles zero amount lines | amount: 0, rate: 1900 | tax = 0 | edge case |
| calculates tax with non-standard rate | net amount: 8999, rate: 700 (7%) | tax = 629 (rounded) | happy path |
| extracts tax correctly for small amounts | gross amount: 119, rate: 1900 | tax = 19, net = 100 | edge case |
| handles high tax rates | net amount: 10000, rate: 2500 (25%) | tax = 2500 | happy path |

---

#### tests/Unit/ShippingCalculatorTest.php

**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| matches a zone by country code | zone countries_json: ["DE","AT","CH"]; address country: "DE" | zone matched | happy path |
| matches a zone by region code | zone regions_json: ["US-NY","US-CA"]; address: country "US", province_code "US-NY" | zone matched | happy path |
| returns empty when no zone matches the address | zone countries_json: ["DE"]; address country: "FR" | empty collection returned | edge case |
| calculates a flat rate | rate type: flat, config: amount 499 | shipping = 499 | happy path |
| calculates a weight-based rate | rate type: weight, config: ranges 0-500g=499, 501-2000g=899; cart total weight: 750g | shipping = 899 | happy path |
| calculates a price-based rate | rate type: price, config: ranges 0-5000=799, 5001-999999=399; cart subtotal: 7500 | shipping = 399 | happy path |
| returns zero shipping when no items require shipping | all cart line variants have requires_shipping = false | shipping = 0 | edge case |
| returns the correct rate when multiple zones match and the first is selected | two zones match, each with different rates | returns rates from both zones for customer selection | edge case |
| skips inactive rates | rate with is_active = 0 | rate not included in available rates | edge case |

---

#### tests/Unit/CartVersionTest.php

**Setup:** Instantiate Cart model or CartService.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| starts at version 1 | create new cart | cart_version = 1 | happy path |
| increments version on add line | add a line to cart | cart_version = 2 | happy path |
| increments version on update quantity | update line quantity | cart_version incremented by 1 | happy path |
| increments version on remove line | remove a line | cart_version incremented by 1 | happy path |


The scope is massive (19,845 lines of specs, 12 phases). I'll set up a team, create progress tracking, then execute phase by phase. Let me start.


TeamCreate

Build complete e-commerce shop system per specs/* (12 phases)


Error: Team "shop-build" already exists at /Users/fabianwesner/.claude/teams/shop-build/config.json. Choose a different team_name, or run TeamDelete on the existing team first.


Bash

cat /Users/fabianwesner/.claude/teams/shop-build/config.json 2>/dev/null; echo "---"; ls /Users/fabianwesner/.claude/tasks/shop-build/ 2>/dev/null

Check existing team config


{
  "name": "shop-build",
  "description": "Building complete e-commerce shop system from specs",
  "createdAt": 1770906082929,
  "leadAgentId": "team-lead@shop-build",
  "leadSessionId": "679ce638-5913-4247-95ec-4008a56d0240",
  "members": [
    {
      "agentId": "team-lead@shop-build",
      "name": "team-lead",
      "agentType": "team-lead",
      "model": "claude-opus-4-6",
      "joinedAt": 1770906082929,
      "tmuxPaneId": "",
      "cwd": "/Users/fabianwesner/Herd/shop",
      "subscriptions": []
    },
    {
      "agentId": "models-and-factories@shop-build",
      "name": "models-and-factories",
      "agentType": "general-purpose",
      "model": "claude-opus-4-6",
      "prompt": "You are building ALL models and factories for an e-commerce shop at /Users/fabianwesner/Herd/shop (Laravel 12). All migrations already exist and pass. You must create:\n\n## CRITICAL RULES\n- Never use em-dash character\n- All monetary amounts stored as INTEGER in minor units (cents)\n- Every model needs: explicit return types, proper $fillable, casts() method, factory\n- Use `php artisan make:model ModelName -f --no-interaction` to create models with factories\n\n## Models to create:\n\n### 1. Update existing User model (app/Models/User.php)\nThe User model already exists. Update it:\n- The `password` column in the DB is actually `password_hash`. Override getAuthPassword() to return $this->password_hash\n- Add $fillable: name, email, password_hash, status, last_login_at\n- Add hidden: password_hash, two_factor_secret, two_factor_recovery_codes, remember_token\n- Add casts: email_verified_at => datetime, password_hash => hashed, last_login_at => datetime, status => string\n- Add relationship: belongsToMany(Store::class, 'store_users')->withPivot('role')->withTimestamps()  (use 'created_at' only)\n- Add method roleForStore(Store $store): ?StoreUserRole - looks up pivot and returns the role enum\n- Add initials() method (already exists, keep it)\n- Update the UserFactory: name (faker name), email (faker unique safe email), password_hash (Hash::make('password')), status ('active'), email_verified_at (now()), last_login_at (faker dateTimeBetween('-30 days'))\n\n### 2. Organization model\n- fillable: name, billing_email\n- hasMany(Store::class)\n- Factory: name (faker company), billing_email (faker companyEmail)\n\n### 3. Store model\n- fillable: organization_id, name, handle, status, default_currency, default_locale, timezone\n- casts: status => StoreStatus enum\n- belongsTo(Organization), hasMany(StoreDomain), belongsToMany(User) through store_users with pivot role, hasOne(StoreSettings)\n- Factory: organization_id (Organization factory), name (faker company + ' Store'), handle (faker unique slug), status ('active'), default_currency ('EUR'), default_locale ('en'), timezone ('Europe/Berlin')\n\n### 4. StoreDomain model\n- fillable: store_id, hostname, type, is_primary, tls_mode\n- casts: type => StoreDomainType enum, is_primary => boolean\n- belongsTo(Store)\n- Factory: store_id (Store factory), hostname (faker unique domainName), type ('storefront'), is_primary (true), tls_mode ('managed')\n\n### 5. StoreUser (Pivot model)\n- Create as app/Models/StoreUser.php extending Illuminate\\Database\\Eloquent\\Relations\\Pivot\n- public $incrementing = false\n- protected $table = 'store_users'\n- casts: role => StoreUserRole enum\n- No factory needed\n\n### 6. StoreSettings model\n- protected $primaryKey = 'store_id'\n- public $incrementing = false\n- fillable: store_id, settings_json\n- casts: settings_json => array\n- belongsTo(Store)\n- IMPORTANT: Only has updated_at, no created_at. Set const CREATED_AT = null\n- Factory: store_id (Store factory), settings_json ([])\n\n### 7. Product model (uses BelongsToStore trait)\n- fillable: store_id, title, handle, status, description_html, vendor, product_type, tags, published_at\n- casts: status => ProductStatus, tags => array, published_at => datetime\n- hasMany(ProductVariant), hasMany(ProductOption), hasMany(ProductMedia), belongsToMany(Collection, 'collection_products')\n- Factory: store_id (Store factory), title (faker words 3 joined), handle (Str::slug of title), status ('active'), description_html (faker paragraphs 2 as html), vendor (faker company), product_type (random of Shirts/Pants/Shoes/Accessories), tags (json_encode [random items]), published_at (now())\n- States: draft (status=draft, published_at=null), archived (status=archived)\n\n### 8. ProductOption model\n- fillable: product_id, name, position\n- belongsTo(Product), hasMany(ProductOptionValue)\n- No timestamps\n- public $timestamps = false\n- Factory: product_id (Product factory), name (faker randomElement ['Size', 'Color', 'Material']), position (0)\n\n### 9. ProductOptionValue model\n- fillable: product_option_id, value, position\n- belongsTo(ProductOption)\n- public $timestamps = false\n- Factory: product_option_id (ProductOption factory), value (faker word), position (0)\n\n### 10. ProductVariant model\n- fillable: product_id, sku, barcode, price_amount, compare_at_amount, currency, weight_g, requires_shipping, is_default, position, status\n- casts: requires_shipping => boolean, is_default => boolean, status => VariantStatus\n- belongsTo(Product), hasOne(InventoryItem, 'variant_id'), belongsToMany(ProductOptionValue, 'variant_option_values', 'variant_id', 'product_option_value_id')\n- Factory: product_id (Product factory), sku (faker bothify 'SKU-####')), price_amount (faker numberBetween(499, 9999)), currency ('EUR'), requires_shipping (true), is_default (false), position (0), status ('active')\n\n### 11. InventoryItem model (uses BelongsToStore trait)\n- fillable: store_id, variant_id, quantity_on_hand, quantity_reserved, policy\n- casts: policy => InventoryPolicy\n- No timestamps\n- public $timestamps = false\n- belongsTo(ProductVariant, 'variant_id')\n- Factory: store_id (Store factory), variant_id (ProductVariant factory), quantity_on_hand (faker numberBetween(0, 100)), quantity_reserved (0), policy ('deny')\n\n### 12. Collection model (uses BelongsToStore trait)\n- fillable: store_id, title, handle, description_html, type, status\n- casts: status => CollectionStatus\n- belongsToMany(Product, 'collection_products')\n- Factory: store_id (Store factory), title (faker words 2 joined), handle (Str::slug of title), type ('manual'), status ('active')\n\n### 13. ProductMedia model\n- fillable: product_id, type, storage_key, alt_text, width, height, mime_type, byte_size, position, status\n- casts: type => MediaType, status => MediaStatus\n- No standard timestamps (only created_at)\n- const UPDATED_AT = null\n- belongsTo(Product)\n- Factory: product_id (Product factory), type ('image'), storage_key (faker filePath), alt_text (faker sentence), width (800), height (600), mime_type ('image/jpeg'), byte_size (faker numberBetween(10000, 500000)), position (0), status ('ready')\n\n### 14. Theme model (uses BelongsToStore trait)\n- fillable: store_id, name, version, status, published_at\n- casts: status => ThemeStatus, published_at => datetime\n- hasMany(ThemeFile), hasOne(ThemeSettings)\n- Factory: store_id (Store factory), name ('Default Theme'), version ('1.0.0'), status ('published'), published_at (now())\n\n### 15. ThemeFile model\n- fillable: theme_id, path, storage_key, sha256, byte_size\n- No timestamps\n- public $timestamps = false\n- belongsTo(Theme)\n\n### 16. ThemeSettings model\n- primaryKey = 'theme_id', incrementing = false\n- fillable: theme_id, settings_json\n- casts: settings_json => array\n- const CREATED_AT = null\n- belongsTo(Theme)\n\n### 17. Page model (uses BelongsToStore trait)\n- fillable: store_id, title, handle, body_html, status, published_at\n- casts: status => PageStatus, published_at => datetime\n- Factory: store_id (Store factory), title (faker words 2 joined), handle (Str::slug of title), status ('published'), body_html (faker paragraphs 3 html), published_at (now())\n\n### 18. NavigationMenu model (uses BelongsToStore trait)\n- fillable: store_id, handle, title\n- hasMany(NavigationItem, 'menu_id')\n- Factory: store_id (Store factory), handle (faker unique slug), title (faker words 2 joined)\n\n### 19. NavigationItem model\n- fillable: menu_id, type, label, url, resource_id, position\n- casts: type => NavigationItemType\n- No timestamps\n- public $timestamps = false\n- belongsTo(NavigationMenu, 'menu_id')\n- Factory: menu_id (NavigationMenu factory), type ('link'), label (faker words 2 joined), url (faker url), position (0)\n\n### 20. Customer model (uses BelongsToStore trait)\n- Implements Authenticatable interface (extends Illuminate\\Foundation\\Auth\\User)\n- fillable: store_id, email, password_hash, name, marketing_opt_in\n- hidden: password_hash\n- casts: marketing_opt_in => boolean, password_hash => hashed\n- getAuthPassword() returns $this->password_hash\n- hasMany(CustomerAddress), hasMany(Order), hasMany(Cart)\n- Factory: store_id (Store factory), email (faker unique safeEmail), password_hash (Hash::make('password')), name (faker name), marketing_opt_in (false)\n\n### 21. CustomerAddress model\n- fillable: customer_id, label, address_json, is_default\n- casts: address_json => array, is_default => boolean\n- No timestamps\n- public $timestamps = false\n- belongsTo(Customer)\n- Factory: customer_id (Customer factory), label ('Home'), address_json (realistic address array), is_default (true)\n\n### 22. Cart model (uses BelongsToStore trait)\n- fillable: store_id, customer_id, currency, cart_version, status\n- casts: status => CartStatus\n- belongsTo(Customer), hasMany(CartLine), hasMany(Checkout)\n- Factory: store_id (Store factory), currency ('EUR'), cart_version (1), status ('active')\n\n### 23. CartLine model\n- fillable: cart_id, variant_id, quantity, unit_price_amount, line_subtotal_amount, line_discount_amount, line_total_amount\n- No timestamps\n- public $timestamps = false\n- belongsTo(Cart), belongsTo(ProductVariant, 'variant_id')\n- Factory: cart_id (Cart factory), variant_id (ProductVariant factory), quantity (1), unit_price_amount (2499), line_subtotal_amount (2499), line_total_amount (2499)\n\n### 24. Checkout model (uses BelongsToStore trait)\n- fillable: store_id, cart_id, customer_id, status, payment_method, email, shipping_address_json, billing_address_json, shipping_method_id, discount_code, tax_provider_snapshot_json, totals_json, expires_at\n- casts: status => CheckoutStatus, payment_method => PaymentMethod, shipping_address_json => array, billing_address_json => array, totals_json => array, expires_at => datetime\n- belongsTo(Cart), belongsTo(Customer)\n\n### 25. ShippingZone model (uses BelongsToStore trait)\n- fillable: store_id, name, countries_json, regions_json\n- casts: countries_json => array, regions_json => array\n- No timestamps, $timestamps = false\n- hasMany(ShippingRate, 'zone_id')\n\n### 26. ShippingRate model\n- fillable: zone_id, name, type, config_json, is_active\n- casts: type => ShippingRateType, config_json => array, is_active => boolean\n- No timestamps, $timestamps = false\n- belongsTo(ShippingZone, 'zone_id')\n\n### 27. TaxSettings model\n- primaryKey = 'store_id', incrementing = false\n- fillable: store_id, mode, provider, prices_include_tax, config_json\n- casts: mode => TaxMode, prices_include_tax => boolean, config_json => array\n- No timestamps, $timestamps = false\n- belongsTo(Store)\n\n### 28. Discount model (uses BelongsToStore trait)\n- fillable: store_id, type, code, value_type, value_amount, starts_at, ends_at, usage_limit, usage_count, rules_json, status\n- casts: type => DiscountType, value_type => DiscountValueType, status => DiscountStatus, starts_at => datetime, ends_at => datetime, rules_json => array\n\n### 29. Order model (uses BelongsToStore trait)\n- fillable: store_id, customer_id, order_number, payment_method, status, financial_status, fulfillment_status, currency, subtotal_amount, discount_amount, shipping_amount, tax_amount, total_amount, email, billing_address_json, shipping_address_json, placed_at\n- casts: payment_method => PaymentMethod, status => OrderStatus, financial_status => FinancialStatus, fulfillment_status => FulfillmentStatus, billing_address_json => array, shipping_address_json => array, placed_at => datetime\n- belongsTo(Customer), hasMany(OrderLine), hasMany(Payment), hasMany(Refund), hasMany(Fulfillment)\n\n### 30. OrderLine model\n- fillable: order_id, product_id, variant_id, title_snapshot, sku_snapshot, quantity, unit_price_amount, total_amount, tax_lines_json, discount_allocations_json\n- casts: tax_lines_json => array, discount_allocations_json => array\n- No timestamps, $timestamps = false\n- belongsTo(Order), belongsTo(Product), belongsTo(ProductVariant, 'variant_id')\n\n### 31. Payment model\n- fillable: order_id, provider, method, provider_payment_id, status, amount, currency, raw_json_encrypted\n- casts: method => PaymentMethod, status => PaymentStatus, raw_json_encrypted => encrypted\n- const UPDATED_AT = null\n- belongsTo(Order)\n\n### 32. Refund model\n- fillable: order_id, payment_id, amount, reason, status, provider_refund_id\n- casts: status => RefundStatus\n- const UPDATED_AT = null\n- belongsTo(Order), belongsTo(Payment)\n\n### 33. Fulfillment model\n- fillable: order_id, status, tracking_company, tracking_number, tracking_url, shipped_at\n- casts: status => FulfillmentShipmentStatus, shipped_at => datetime\n- const UPDATED_AT = null\n- belongsTo(Order), hasMany(FulfillmentLine)\n\n### 34. FulfillmentLine model\n- fillable: fulfillment_id, order_line_id, quantity\n- No timestamps, $timestamps = false\n- belongsTo(Fulfillment), belongsTo(OrderLine)\n\n### 35. AnalyticsEvent model (uses BelongsToStore trait)\n- table: analytics_events\n- fillable: store_id, type, session_id, customer_id, properties_json, client_event_id, occurred_at\n- casts: properties_json => array, occurred_at => datetime\n- const UPDATED_AT = null\n\n### 36. AnalyticsDaily model (uses BelongsToStore trait)\n- table: analytics_daily\n- Composite primary key - no autoincrement id\n- $incrementing = false\n- $timestamps = false\n- fillable: store_id, date, orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count, checkout_completed_count\n\n### 37-40. App, AppInstallation, WebhookSubscription, WebhookDelivery\nCreate simple models for the apps/webhooks tables with appropriate relationships and fillable.\n\n## BelongsToStore trait\nCreate `app/Models/Concerns/BelongsToStore.php`:\n```php\ntrait BelongsToStore {\n    public static function bootBelongsToStore(): void {\n        static::addGlobalScope(new StoreScope);\n        static::creating(function ($model) {\n            if (!$model->store_id && app()->bound('current_store')) {\n                $model->store_id = app('current_store')->id;\n            }\n        });\n    }\n    public function store(): BelongsTo { return $this->belongsTo(Store::class); }\n}\n```\n\n## StoreScope\nCreate `app/Models/Scopes/StoreScope.php`:\n```php\nclass StoreScope implements Scope {\n    public function apply(Builder $builder, Model $model): void {\n        if (app()->bound('current_store')) {\n            $builder->where($model->getTable().'.store_id', app('current_store')->id);\n        }\n    }\n}\n```\n\nAfter creating all models and factories, run:\n1. `vendor/bin/pint --dirty` to fix code style\n2. `php artisan migrate:fresh` to verify everything still works\n\nDO NOT write tests - just create the models and factories.",
      "color": "green",
      "planModeRequired": false,
      "joinedAt": 1770906481855,
      "tmuxPaneId": "in-process",
      "cwd": "/Users/fabianwesner/Herd/shop",
      "subscriptions": [],
      "backendType": "in-process"
    },
    {
      "agentId": "middleware-auth-services@shop-build",
      "name": "middleware-auth-services",
      "agentType": "general-purpose",
      "model": "claude-opus-4-6",
      "prompt": "You are building the middleware, authentication, authorization, and core services for an e-commerce shop at /Users/fabianwesner/Herd/shop (Laravel 12 + Livewire v4). All migrations exist and pass. Models are being created by another agent.\n\n## CRITICAL RULES\n- Never use the em-dash character\n- Read existing files before modifying them\n- Use proper PHP 8.4 features\n\n## 1. ResolveStore Middleware\n\nCreate `app/Http/Middleware/ResolveStore.php`:\n\n```php\nclass ResolveStore\n{\n    public function handle(Request $request, Closure $next, string $type = 'storefront'): Response\n    {\n        if ($type === 'storefront') {\n            return $this->resolveFromHostname($request, $next);\n        }\n        return $this->resolveFromSession($request, $next);\n    }\n    \n    private function resolveFromHostname($request, $next)\n    {\n        $hostname = $request->getHost();\n        $storeId = Cache::remember(\"store_domain:{$hostname}\", 300, function () use ($hostname) {\n            return StoreDomain::where('hostname', $hostname)->value('store_id');\n        });\n        \n        if (!$storeId) { abort(404); }\n        \n        $store = Store::find($storeId);\n        if (!$store) { abort(404); }\n        if ($store->status === StoreStatus::Suspended) { abort(503); }\n        \n        app()->instance('current_store', $store);\n        View::share('currentStore', $store);\n        \n        return $next($request);\n    }\n    \n    private function resolveFromSession($request, $next) \n    {\n        $storeId = session('current_store_id');\n        if (!$storeId) { abort(403, 'No store selected'); }\n        \n        $user = $request->user();\n        if (!$user) { return redirect('/admin/login'); }\n        \n        $hasAccess = DB::table('store_users')\n            ->where('store_id', $storeId)\n            ->where('user_id', $user->id)\n            ->exists();\n        if (!$hasAccess) { abort(403); }\n        \n        $store = Store::find($storeId);\n        if (!$store) { abort(403); }\n        \n        app()->instance('current_store', $store);\n        View::share('currentStore', $store);\n        \n        return $next($request);\n    }\n}\n```\n\n## 2. Update bootstrap/app.php\n\nRead the EXISTING bootstrap/app.php first, then update it:\n- Register middleware aliases: 'store.resolve' => ResolveStore::class\n- Set redirect targets: guests => '/admin/login', users => '/admin'\n- Add API routes file registration\n\n## 3. Admin Auth Login Livewire Component\n\nCreate `app/Livewire/Admin/Auth/Login.php`:\n- Properties: email, password, remember (bool)\n- Method login(): validates, rate-limits (5/min per IP), attempts auth with web guard\n- On success: regenerate session, look up user's first store via store_users, set session('current_store_id'), redirect /admin\n- On failure: add error 'Invalid credentials'\n- Uses a simple Blade view at resources/views/livewire/admin/auth/login.blade.php\n- The layout should be a minimal auth layout (not the full admin layout)\n\nCreate the Blade view with Flux UI components:\n- flux:input for email, flux:input type=password, flux:checkbox for remember, flux:button for submit\n- Show validation errors\n- Link to forgot password\n\n## 4. Admin Auth Layout\n\nCreate `resources/views/components/layouts/admin-auth.blade.php`:\n- Minimal centered layout with card for auth forms\n- Include @livewireStyles, @livewireScripts, @vite\n\n## 5. Customer Auth Provider\n\nCreate `app/Auth/CustomerUserProvider.php`:\n- Implements Illuminate\\Contracts\\Auth\\UserProvider\n- Constructor takes model class name\n- retrieveById, retrieveByToken, updateRememberToken, retrieveByCredentials, validateCredentials\n- retrieveByCredentials: must inject store_id from current_store into the query\n- Register in AppServiceProvider: Auth::provider('customer', fn($app, $config) => new CustomerUserProvider($config['model']))\n\n## 6. AppServiceProvider Updates\n\nUpdate `app/Providers/AppServiceProvider.php`:\n- Register the customer auth provider\n- Register rate limiters for login (5 per minute per IP)\n- Bind PaymentProvider interface to MockPaymentProvider (stub for now)\n\n## 7. Routes (routes/web.php)\n\nReplace the entire routes/web.php with proper route definitions:\n\n```php\n// Admin auth routes (no store.resolve needed)\nRoute::prefix('admin')->group(function () {\n    Route::get('login', \\App\\Livewire\\Admin\\Auth\\Login::class)->name('admin.login')->middleware('guest');\n    Route::post('logout', function () {\n        Auth::guard('web')->logout();\n        request()->session()->invalidate();\n        request()->session()->regenerateToken();\n        return redirect('/admin/login');\n    })->name('admin.logout')->middleware('auth');\n});\n\n// Admin routes (auth + store resolution)\nRoute::prefix('admin')->middleware(['auth', 'verified', 'store.resolve:admin'])->group(function () {\n    Route::get('/', AdminDashboard::class)->name('admin.dashboard');\n    // ... all admin routes\n});\n\n// Storefront routes\nRoute::middleware(['store.resolve:storefront'])->group(function () {\n    Route::get('/', StorefrontHome::class)->name('storefront.home');\n    // ... all storefront routes\n});\n```\n\nNOTE: Many Livewire components don't exist yet - that's OK. Use class string references and they'll be created later. What's important is that the routes are defined correctly.\n\n## 8. Authorization Policies\n\nCreate policies for each resource. Each checks user role via store_users:\n\nPermission matrix:\n- Owner: everything\n- Admin: everything except delete store\n- Staff: manage products, orders (CRUD), discounts, fulfillments, view analytics, view/update customers\n- Support: read-only orders, view customers\n\nCreate a base helper trait `app/Policies/Concerns/ChecksStoreRole.php`:\n```php\ntrait ChecksStoreRole {\n    protected function getUserRole(User $user): ?StoreUserRole {\n        if (!app()->bound('current_store')) return null;\n        return $user->roleForStore(app('current_store'));\n    }\n    protected function hasMinRole(User $user, StoreUserRole ...$roles): bool {\n        $role = $this->getUserRole($user);\n        return $role && in_array($role, $roles);\n    }\n}\n```\n\nCreate policies: ProductPolicy, OrderPolicy, CollectionPolicy, DiscountPolicy, CustomerPolicy, StorePolicy, PagePolicy, ThemePolicy, FulfillmentPolicy, RefundPolicy.\n\nAfter creating everything, run `vendor/bin/pint --dirty` to fix code style.\n\nDO NOT create test files - just the production code.",
      "color": "yellow",
      "planModeRequired": false,
      "joinedAt": 1770906514465,
      "tmuxPaneId": "in-process",
      "cwd": "/Users/fabianwesner/Herd/shop",
      "subscriptions": [],
      "backendType": "in-process"
    }
  ]
}---
1.json
2.json
20.json
21.json
22.json
3.json
4.json
5.json
6.json
7.json
8.json
9.json


No tasks found


ls /Users/fabianwesner/.claude/tasks/shop-build/*.json 2>/dev/null | head -5; echo "---"; cat /Users/fabianwesner/.claude/tasks/shop-build/1.json 2>/dev/null

Check existing task state


/Users/fabianwesner/.claude/tasks/shop-build/1.json
/Users/fabianwesner/.claude/tasks/shop-build/2.json
/Users/fabianwesner/.claude/tasks/shop-build/20.json
/Users/fabianwesner/.claude/tasks/shop-build/21.json
/Users/fabianwesner/.claude/tasks/shop-build/22.json
---
{
  "id": "1",
  "subject": "Phase 1: Foundation - Migrations, Models, Middleware, Auth",
  "description": "Implement Phase 1 of the shop system: Environment config, core migrations (organizations, stores, store_domains, users modification, store_users, store_settings), core models with factories/seeders, enums (StoreStatus, StoreUserRole, StoreDomainType), tenant resolution middleware (ResolveStore), BelongsToStore trait and StoreScope, admin auth (Login/Logout Livewire components), customer auth (custom guard, CustomerUserProvider), authorization policies. Must follow specs/01-DATABASE-SCHEMA.md, specs/05-BUSINESS-LOGIC.md, specs/06-AUTH-AND-SECURITY.md, and specs/09-IMPLEMENTATION-ROADMAP.md Phase 1.",
  "activeForm": "Building Phase 1 Foundation",
  "owner": "team-lead",
  "status": "completed",
  "blocks": [
    "2",
    "3"
  ],
  "blockedBy": []
}


ls /Users/fabianwesner/Herd/shop/database/migrations/ | head -20; echo "---"; ls /Users/fabianwesner/Herd/shop/app/Models/; echo "---"; ls /Users/fabianwesner/Herd/shop/app/Enums/ 2>/dev/null; echo "---"; ls /Users/fabianwesner/Herd/shop/app/Services/ 2>/dev/null; echo "---"; ls /Users/fabianwesner/Herd/shop/app/Http/Middleware/ 2>/dev/null; echo "---"; cat /Users/fabianwesner/.claude/tasks/shop-build/20.json 2>/dev/null

Check actual state of phase 1 work


0001_01_01_000000_create_users_table.php
0001_01_01_000001_create_cache_table.php
0001_01_01_000002_create_jobs_table.php
2025_08_14_170933_add_two_factor_columns_to_users_table.php
---
User.php
---
---
Tax
---
---
{
  "id": "20",
  "subject": "models-and-factories",
  "description": "You are building ALL models and factories for an e-commerce shop at /Users/fabianwesner/Herd/shop (L",
  "status": "completed",
  "blocks": [],
  "blockedBy": [],
  "metadata": {
    "_internal": true
  }
}


A previous incomplete team run exists. Let me clean it up and start fresh.


ToolSearch

Question: select:TeamDelete


Question: select:TeamDelete


No team name found, nothing to clean up

No team name found, nothing to clean up


Bash

rm -rf /Users/fabianwesner/.claude/teams/shop-build /Users/fabianwesner/.claude/tasks/shop-build && echo "cleaned"

Clean stale team data


Error: Exit code 1
rm: /Users/fabianwesner/.claude/teams/shop-build/config.json: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build/inboxes/phase1-foundation.json: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build/inboxes/models-and-factories.json: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build/inboxes/middleware-auth-services.json: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build/inboxes/team-lead.json: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build/inboxes: Operation not permitted
rm: /Users/fabianwesner/.claude/teams/shop-build: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/.lock: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/20.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/6.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/7.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/21.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/1.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/2.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/3.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/8.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/22.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/4.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/.highwatermark: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/5.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build/9.json: Operation not permitted
rm: /Users/fabianwesner/.claude/tasks/shop-build: Operation not permitted


TeamCreate

Full e-commerce shop build per specs/* phases 1-12


Write

# Shop Build Progress

Tracking progress for the full shop system implementation (team: shop-v2).

## Phase Checklist

- [ ] Phase 1: Foundation (migrations, models, middleware, auth, authorization)
- [ ] Phase 2: Catalog (products, variants, inventory, collections, media)
- [ ] Phase 3: Themes, pages, navigation, storefront layout
- [ ] Phase 4: Cart, checkout, discounts, shipping, taxes
- [ ] Phase 5: Payments, orders, fulfillment
- [ ] Phase 6: Customer accounts
- [ ] Phase 7: Admin panel
- [ ] Phase 8: Search
- [ ] Phase 9: Analytics
- [ ] Phase 10: Apps and webhooks
- [ ] Phase 11: Polish
- [ ] Phase 12: Full test suite execution + browser review

## Log

- Starting Phase 1: Foundation


File created successfully at: /Users/fabianwesner/Herd/shop/specs/progress.md

# Shop Build Progress

Tracking progress for the full shop system implementation (team: shop-v2).

## Phase Checklist

- [ ] Phase 1: Foundation (migrations, models, middleware, auth, authorization)
- [ ] Phase 2: Catalog (products, variants, inventory, collections, media)
- [ ] Phase 3: Themes, pages, navigation, storefront layout
- [ ] Phase 4: Cart, checkout, discounts, shipping, taxes
- [ ] Phase 5: Payments, orders, fulfillment
- [ ] Phase 6: Customer accounts
- [ ] Phase 7: Admin panel
- [ ] Phase 8: Search
- [ ] Phase 9: Analytics
- [ ] Phase 10: Apps and webhooks
- [ ] Phase 11: Polish
- [ ] Phase 12: Full test suite execution + browser review

## Log

- Starting Phase 1: Foundation


TaskCreate

Phase 1: Foundation

Implement Phase 1 per specs/09-IMPLEMENTATION-ROADMAP.md: env/config, core migrations (organizations, stores, store_domains, users modifications, store_users, store_settings), core models with factories, enums (StoreStatus, StoreUserRole, StoreDomainType), ResolveStore middleware, BelongsToStore trait + StoreScope, admin auth (Login Livewire component), customer auth (custom guard + CustomerUserProvider), authorization policies, Pest tests for tenancy/auth.


Task #1 created successfully: Phase 1: Foundation

Phase 1: Foundation


Updated task #1 owner, status


Agent

Phase 1 Foundation implementer

Prompt:
You are a Laravel 12 backend engineer building Phase 1 of a multi-tenant e-commerce shop at /Users/fabianwesner/Herd/shop. This is a one-shot autonomous build - do NOT stop until all of Phase 1 is complete and tests pass.

## CRITICAL RULES
1. NEVER use the em-dash character (—) in any text or code
2. Every PHP file MUST have explicit return types and proper type hints (PHP 8.4)
3. Use `php artisan make:*` commands to scaffold files
4. After all PHP edits, run `vendor/bin/pint --dirty --format agent`
5. All monetary amounts in minor units (cents) as INTEGER
6. Use `fake()` helper, not `$this->faker`
7. Check existing files with Read before editing
8. Commit nothing - team-lead handles commits

## Phase 1 Scope (from specs/09-IMPLEMENTATION-ROADMAP.md)

### Step 1.1: Environment Config
Update `.env`:
- DB_CONNECTION=sqlite
- DB_DATABASE=database/database.sqlite  (file must exist - `touch` it)
- CACHE_STORE=file, SESSION_DRIVER=file, QUEUE_CONNECTION=sync
- MAIL_MAILER=log, FILESYSTEM_DISK=local
Update `config/auth.php`: Add `customer` guard with `customers` provider pointing to Customer model. Add customer password broker. Keep existing web guard.

### Step 1.2: Core Migrations
Read `specs/01-DATABASE-SCHEMA.md` lines 1-400 for exact column definitions. Create migrations in this order (use timestamp prefix 2026_04_12_HHMMSS_ style):
1. `create_organizations_table` - id, name, billing_email, created_at, updated_at
2. `create_stores_table` - id, organization_id (FK), name, handle (unique), status (text, check), default_currency, default_locale, timezone, created_at, updated_at
3. `create_store_domains_table` - id, store_id (FK), hostname (unique), type (text check: storefront/admin/api), is_primary (bool), tls_mode (text), created_at, updated_at
4. `modify_users_table_for_shop` - add: status (text default 'active'), last_login_at (timestamp nullable). DO NOT rename password (keep as 'password' for Laravel compatibility - the spec's "password_hash" is internal naming, we use Laravel default)
5. `create_store_users_table` - store_id, user_id (composite PK), role (text check), created_at
6. `create_store_settings_table` - store_id (PK), settings_json (text), updated_at

### Step 1.3: Enums
Create in `app/Enums/`:
- `StoreStatus` (string-backed): Active='active', Suspended='suspended'
- `StoreUserRole` (string-backed): Owner='owner', Admin='admin', Staff='staff', Support='support'
- `StoreDomainType` (string-backed): Storefront='storefront', Admin='admin', Api='api'

### Step 1.4: Core Models
Use `php artisan make:model Name -f` (with factory).

**Organization**: fillable=[name, billing_email]; hasMany(Store).

**Store**: fillable=[organization_id, name, handle, status, default_currency, default_locale, timezone]; casts: status=>StoreStatus. Relations: belongsTo(Organization), hasMany(StoreDomain), belongsToMany(User, 'store_users')->withPivot('role')->using(StoreUser::class), hasOne(StoreSettings).

**StoreDomain**: fillable=[store_id, hostname, type, is_primary, tls_mode]; casts: type=>StoreDomainType, is_primary=>bool. belongsTo(Store).

**StoreUser** (pivot): extends `Illuminate\Database\Eloquent\Relations\Pivot`. protected $table='store_users'; casts: role=>StoreUserRole; public $incrementing=false; no updated_at (const UPDATED_AT = null).

**StoreSettings**: protected $primaryKey='store_id'; $incrementing=false; fillable=[store_id, settings_json]; casts: settings_json=>array; const CREATED_AT=null; belongsTo(Store).

**Update User model** (app/Models/User.php):
- Add $fillable: status, last_login_at (plus existing name, email, password)
- Add $casts: status=>'string', last_login_at=>'datetime'
- Add belongsToMany(Store::class, 'store_users')->using(StoreUser::class)->withPivot('role') (no timestamps)
- Add method `roleForStore(Store $store): ?StoreUserRole` that looks up pivot
- Keep Fortify 2FA stuff intact

### Step 1.5: Factories
- OrganizationFactory: name=fake()->company(), billing_email=fake()->companyEmail()
- StoreFactory: organization_id=Organization::factory(), name=fake()->company().' Store', handle=Str::slug(unique), status='active', default_currency='EUR', default_locale='en', timezone='Europe/Berlin'
- StoreDomainFactory: store_id=Store::factory(), hostname=fake()->unique()->domainName(), type='storefront', is_primary=true, tls_mode='managed'
- StoreSettingsFactory: store_id=Store::factory(), settings_json=[]
- Update UserFactory: add status='active', last_login_at=now()

### Step 1.6: BelongsToStore Trait + StoreScope
Create `app/Models/Concerns/BelongsToStore.php` trait:
- bootBelongsToStore: addGlobalScope(StoreScope), creating event sets store_id from app('current_store') if not set
- store() belongsTo relation

Create `app/Models/Scopes/StoreScope.php`:
- Implements Illuminate\Database\Eloquent\Scope
- apply(): if app()->bound('current_store'), where(table.store_id, current_store->id)

### Step 1.7: ResolveStore Middleware
Create `app/Http/Middleware/ResolveStore.php`:
```php
public function handle(Request $request, Closure $next, string $type = 'storefront')
```
- storefront: lookup by $request->getHost() via store_domains cache (5min), abort 404 if not found, abort 503 if store suspended, bind 'current_store' in container.
- admin: read session('current_store_id'), verify user has store_users record, bind 'current_store'.

Register in `bootstrap/app.php` withMiddleware():
```php
$middleware->alias(['store.resolve' => \App\Http\Middleware\ResolveStore::class]);
```

### Step 1.8: Admin Auth
Create Livewire components:
- `app/Livewire/Admin/Auth/Login.php` with corresponding blade view at `resources/views/livewire/admin/auth/login.blade.php`
  - Properties: email, password, remember
  - login() action: validate, rate limit login key (5/min per IP), Auth::guard('web')->attempt(), session regen, pick user's first store_id -> session('current_store_id'), redirect /admin
  - Use Flux UI components: flux:input, flux:checkbox, flux:button
  - Layout: `components.layouts.admin-auth` (create this layout file as a minimal centered card with @livewireStyles/Scripts, @vite, flux scripts)

Create `app/Providers/RouteServiceProvider.php` if needed OR register in AppServiceProvider boot():
```php
RateLimiter::for('login', fn ($req) => Limit::perMinute(5)->by($req->ip()));
```

### Step 1.9: Customer Auth Provider
Create `app/Auth/CustomerUserProvider.php` implementing `Illuminate\Contracts\Auth\UserProvider`:
- Standard methods but retrieveByCredentials adds where store_id = app('current_store')->id
- Register in AppServiceProvider::boot():
```php
Auth::provider('customer', fn ($app, $config) => new CustomerUserProvider($config['model']));
```
(Customer model doesn't exist yet - that's OK, comment: "Customer model created in Phase 6. Guard configured in advance.")

### Step 1.10: Authorization Policies
Create `app/Policies/Concerns/ChecksStoreRole.php` trait:
```php
protected function getUserRole(User $user): ?StoreUserRole {
    if (!app()->bound('current_store')) return null;
    return $user->roleForStore(app('current_store'));
}
protected function hasMinRole(User $user, StoreUserRole ...$roles): bool {
    $role = $this->getUserRole($user);
    return $role !== null && in_array($role, $roles, true);
}
```

Create policies (skeletons acceptable - actual resources created in later phases):
- StorePolicy (view, update, delete: check role)
- Other policies ProductPolicy, OrderPolicy, CollectionPolicy, DiscountPolicy, CustomerPolicy, PagePolicy, ThemePolicy, FulfillmentPolicy, RefundPolicy will be created in their respective phases - SKIP for Phase 1.

Only create StorePolicy + the ChecksStoreRole trait for now.

### Step 1.11: Routes
Update `routes/web.php`:
- Keep the welcome/dashboard routes temporarily but namespace them for now
- Add admin login route: `Route::get('/admin/login', \App\Livewire\Admin\Auth\Login::class)->name('admin.login')->middleware('guest')`
- Add admin logout route (POST /admin/logout)
- DO NOT yet add store.resolve middleware to routes (nothing to resolve to). Leave it for Phase 2.

### Step 1.12: Pest Tests (Phase 1)
Create these tests and verify they pass:

1. `tests/Feature/Tenancy/TenantResolutionTest.php`
   - it resolves store from hostname via store_domains
   - it returns 404 for unknown hostnames
   - it returns 503 for suspended stores
   - it caches hostname lookup

2. `tests/Feature/Tenancy/StoreIsolationTest.php`
   - it applies store scope to models using BelongsToStore (use Store model directly since others don't exist yet - or make a simple test model)

3. `tests/Feature/Auth/AdminAuthTest.php`
   - admin can login with valid credentials
   - admin cannot login with wrong password
   - admin login is rate limited after 5 attempts

Use `RefreshDatabase` trait. Create fake stores and store_domains via factories. For middleware tests, use `withoutMiddleware()` where appropriate.

## Execution Order
1. Update .env + config/auth.php
2. Run `touch database/database.sqlite`
3. Create enums
4. Create migrations (no models yet)
5. Run `php artisan migrate:fresh`
6. Create models + factories
7. Create trait + scope
8. Create middleware
9. Register middleware alias in bootstrap/app.php
10. Create admin Login livewire component + layout + view
11. Create customer auth provider
12. Update routes/web.php
13. Write tests in tests/Feature/Tenancy and tests/Feature/Auth
14. Run `php artisan test --compact`. Fix until green.
15. Run `vendor/bin/pint --dirty --format agent`
16. Run `php artisan migrate:fresh` to verify
17. Report back with: list of files created, test results, any caveats

## Reporting
When done, report to team-lead via SendMessage with:
- Test results (passed/failed count)
- Key files created
- Any spec ambiguities or decisions made
- Blockers (if any)

Do NOT commit. Do NOT proceed past Phase 1. Team-lead will review and commit.

Start now. Work autonomously. If you hit an error, investigate root cause and fix rather than skipping. Mark task #1 completed when all Phase 1 tests pass.


Spawned successfully.
agent_id: phase1-foundation@shop-v2
name: phase1-foundation
team_name: shop-v2
The agent is now running and will receive instructions via mailbox.

Prompt:
You are a Laravel 12 backend engineer building Phase 1 of a multi-tenant e-commerce shop at /Users/fabianwesner/Herd/shop. This is a one-shot autonomous build - do NOT stop until all of Phase 1 is complete and tests pass.

## CRITICAL RULES
1. NEVER use the em-dash character (—) in any text or code
2. Every PHP file MUST have explicit return types and proper type hints (PHP 8.4)
3. Use `php artisan make:*` commands to scaffold files
4. After all PHP edits, run `vendor/bin/pint --dirty --format agent`
5. All monetary amounts in minor units (cents) as INTEGER
6. Use `fake()` helper, not `$this->faker`
7. Check existing files with Read before editing
8. Commit nothing - team-lead handles commits

## Phase 1 Scope (from specs/09-IMPLEMENTATION-ROADMAP.md)

### Step 1.1: Environment Config
Update `.env`:
- DB_CONNECTION=sqlite
- DB_DATABASE=database/database.sqlite  (file must exist - `touch` it)
- CACHE_STORE=file, SESSION_DRIVER=file, QUEUE_CONNECTION=sync
- MAIL_MAILER=log, FILESYSTEM_DISK=local
Update `config/auth.php`: Add `customer` guard with `customers` provider pointing to Customer model. Add customer password broker. Keep existing web guard.

### Step 1.2: Core Migrations
Read `specs/01-DATABASE-SCHEMA.md` lines 1-400 for exact column definitions. Create migrations in this order (use timestamp prefix 2026_04_12_HHMMSS_ style):
1. `create_organizations_table` - id, name, billing_email, created_at, updated_at
2. `create_stores_table` - id, organization_id (FK), name, handle (unique), status (text, check), default_currency, default_locale, timezone, created_at, updated_at
3. `create_store_domains_table` - id, store_id (FK), hostname (unique), type (text check: storefront/admin/api), is_primary (bool), tls_mode (text), created_at, updated_at
4. `modify_users_table_for_shop` - add: status (text default 'active'), last_login_at (timestamp nullable). DO NOT rename password (keep as 'password' for Laravel compatibility - the spec's "password_hash" is internal naming, we use Laravel default)
5. `create_store_users_table` - store_id, user_id (composite PK), role (text check), created_at
6. `create_store_settings_table` - store_id (PK), settings_json (text), updated_at

### Step 1.3: Enums
Create in `app/Enums/`:
- `StoreStatus` (string-backed): Active='active', Suspended='suspended'
- `StoreUserRole` (string-backed): Owner='owner', Admin='admin', Staff='staff', Support='support'
- `StoreDomainType` (string-backed): Storefront='storefront', Admin='admin', Api='api'

### Step 1.4: Core Models
Use `php artisan make:model Name -f` (with factory).

**Organization**: fillable=[name, billing_email]; hasMany(Store).

**Store**: fillable=[organization_id, name, handle, status, default_currency, default_locale, timezone]; casts: status=>StoreStatus. Relations: belongsTo(Organization), hasMany(StoreDomain), belongsToMany(User, 'store_users')->withPivot('role')->using(StoreUser::class), hasOne(StoreSettings).

**StoreDomain**: fillable=[store_id, hostname, type, is_primary, tls_mode]; casts: type=>StoreDomainType, is_primary=>bool. belongsTo(Store).

**StoreUser** (pivot): extends `Illuminate\Database\Eloquent\Relations\Pivot`. protected $table='store_users'; casts: role=>StoreUserRole; public $incrementing=false; no updated_at (const UPDATED_AT = null).

**StoreSettings**: protected $primaryKey='store_id'; $incrementing=false; fillable=[store_id, settings_json]; casts: settings_json=>array; const CREATED_AT=null; belongsTo(Store).

**Update User model** (app/Models/User.php):
- Add $fillable: status, last_login_at (plus existing name, email, password)
- Add $casts: status=>'string', last_login_at=>'datetime'
- Add belongsToMany(Store::class, 'store_users')->using(StoreUser::class)->withPivot('role') (no timestamps)
- Add method `roleForStore(Store $store): ?StoreUserRole` that looks up pivot
- Keep Fortify 2FA stuff intact

### Step 1.5: Factories
- OrganizationFactory: name=fake()->company(), billing_email=fake()->companyEmail()
- StoreFactory: organization_id=Organization::factory(), name=fake()->company().' Store', handle=Str::slug(unique), status='active', default_currency='EUR', default_locale='en', timezone='Europe/Berlin'
- StoreDomainFactory: store_id=Store::factory(), hostname=fake()->unique()->domainName(), type='storefront', is_primary=true, tls_mode='managed'
- StoreSettingsFactory: store_id=Store::factory(), settings_json=[]
- Update UserFactory: add status='active', last_login_at=now()

### Step 1.6: BelongsToStore Trait + StoreScope
Create `app/Models/Concerns/BelongsToStore.php` trait:
- bootBelongsToStore: addGlobalScope(StoreScope), creating event sets store_id from app('current_store') if not set
- store() belongsTo relation

Create `app/Models/Scopes/StoreScope.php`:
- Implements Illuminate\Database\Eloquent\Scope
- apply(): if app()->bound('current_store'), where(table.store_id, current_store->id)

### Step 1.7: ResolveStore Middleware
Create `app/Http/Middleware/ResolveStore.php`:
```php
public function handle(Request $request, Closure $next, string $type = 'storefront')
```
- storefront: lookup by $request->getHost() via store_domains cache (5min), abort 404 if not found, abort 503 if store suspended, bind 'current_store' in container.
- admin: read session('current_store_id'), verify user has store_users record, bind 'current_store'.

Register in `bootstrap/app.php` withMiddleware():
```php
$middleware->alias(['store.resolve' => \App\Http\Middleware\ResolveStore::class]);
```

### Step 1.8: Admin Auth
Create Livewire components:
- `app/Livewire/Admin/Auth/Login.php` with corresponding blade view at `resources/views/livewire/admin/auth/login.blade.php`
  - Properties: email, password, remember
  - login() action: validate, rate limit login key (5/min per IP), Auth::guard('web')->attempt(), session regen, pick user's first store_id -> session('current_store_id'), redirect /admin
  - Use Flux UI components: flux:input, flux:checkbox, flux:button
  - Layout: `components.layouts.admin-auth` (create this layout file as a minimal centered card with @livewireStyles/Scripts, @vite, flux scripts)

Create `app/Providers/RouteServiceProvider.php` if needed OR register in AppServiceProvider boot():
```php
RateLimiter::for('login', fn ($req) => Limit::perMinute(5)->by($req->ip()));
```

### Step 1.9: Customer Auth Provider
Create `app/Auth/CustomerUserProvider.php` implementing `Illuminate\Contracts\Auth\UserProvider`:
- Standard methods but retrieveByCredentials adds where store_id = app('current_store')->id
- Register in AppServiceProvider::boot():
```php
Auth::provider('customer', fn ($app, $config) => new CustomerUserProvider($config['model']));
```
(Customer model doesn't exist yet - that's OK, comment: "Customer model created in Phase 6. Guard configured in advance.")

### Step 1.10: Authorization Policies
Create `app/Policies/Concerns/ChecksStoreRole.php` trait:
```php
protected function getUserRole(User $user): ?StoreUserRole {
    if (!app()->bound('current_store')) return null;
    return $user->roleForStore(app('current_store'));
}
protected function hasMinRole(User $user, StoreUserRole ...$roles): bool {
    $role = $this->getUserRole($user);
    return $role !== null && in_array($role, $roles, true);
}
```

Create policies (skeletons acceptable - actual resources created in later phases):
- StorePolicy (view, update, delete: check role)
- Other policies ProductPolicy, OrderPolicy, CollectionPolicy, DiscountPolicy, CustomerPolicy, PagePolicy, ThemePolicy, FulfillmentPolicy, RefundPolicy will be created in their respective phases - SKIP for Phase 1.

Only create StorePolicy + the ChecksStoreRole trait for now.

### Step 1.11: Routes
Update `routes/web.php`:
- Keep the welcome/dashboard routes temporarily but namespace them for now
- Add admin login route: `Route::get('/admin/login', \App\Livewire\Admin\Auth\Login::class)->name('admin.login')->middleware('guest')`
- Add admin logout route (POST /admin/logout)
- DO NOT yet add store.resolve middleware to routes (nothing to resolve to). Leave it for Phase 2.

### Step 1.12: Pest Tests (Phase 1)
Create these tests and verify they pass:

1. `tests/Feature/Tenancy/TenantResolutionTest.php`
   - it resolves store from hostname via store_domains
   - it returns 404 for unknown hostnames
   - it returns 503 for suspended stores
   - it caches hostname lookup

2. `tests/Feature/Tenancy/StoreIsolationTest.php`
   - it applies store scope to models using BelongsToStore (use Store model directly since others don't exist yet - or make a simple test model)

3. `tests/Feature/Auth/AdminAuthTest.php`
   - admin can login with valid credentials
   - admin cannot login with wrong password
   - admin login is rate limited after 5 attempts

Use `RefreshDatabase` trait. Create fake stores and store_domains via factories. For middleware tests, use `withoutMiddleware()` where appropriate.

## Execution Order
1. Update .env + config/auth.php
2. Run `touch database/database.sqlite`
3. Create enums
4. Create migrations (no models yet)
5. Run `php artisan migrate:fresh`
6. Create models + factories
7. Create trait + scope
8. Create middleware
9. Register middleware alias in bootstrap/app.php
10. Create admin Login livewire component + layout + view
11. Create customer auth provider
12. Update routes/web.php
13. Write tests in tests/Feature/Tenancy and tests/Feature/Auth
14. Run `php artisan test --compact`. Fix until green.
15. Run `vendor/bin/pint --dirty --format agent`
16. Run `php artisan migrate:fresh` to verify
17. Report back with: list of files created, test results, any caveats

## Reporting
When done, report to team-lead via SendMessage with:
- Test results (passed/failed count)
- Key files created
- Any spec ambiguities or decisions made
- Blockers (if any)

Do NOT commit. Do NOT proceed past Phase 1. Team-lead will review and commit.

Start now. Work autonomously. If you hit an error, investigate root cause and fix rather than skipping. Mark task #1 completed when all Phase 1 tests pass.


Phase 1 worker is spawned and running. I'll wait for it to report back.


<teammate-message teammate_id="phase1-foundation" color="blue" summary="Phase 1 complete: 42 tests pass, 0 failures">
Phase 1 Foundation is complete. All 42 tests pass (98 assertions, 0 failures). Full file list and decisions below.

## Test Results
`php artisan test --compact`: 42 passed (98 assertions), 0 failed. Includes all pre-existing tests plus the new Phase 1 ones:
- tests/Feature/Tenancy/TenantResolutionTest.php (4 tests)
- tests/Feature/Tenancy/StoreIsolationTest.php (2 tests)
- tests/Feature/Auth/AdminAuthTest.php (3 tests)

Pint: clean (`{"result":"pass"}`).
Fresh migrate: clean.

## Files Created

Enums (/Users/fabianwesner/Herd/shop/app/Enums):
- StoreStatus.php
- StoreUserRole.php
- StoreDomainType.php

Migrations (/Users/fabianwesner/Herd/shop/database/migrations):
- 2026_04_12_100001_create_organizations_table.php
- 2026_04_12_100002_create_stores_table.php (with status CHECK trigger)
- 2026_04_12_100003_create_store_domains_table.php (with type/tls CHECK triggers)
- 2026_04_12_100004_modify_users_table_for_shop.php (adds status + last_login_at; keeps Laravel `password`)
- 2026_04_12_100005_create_store_users_table.php (composite PK, role CHECK trigger)
- 2026_04_12_100006_create_store_settings_table.php

Models (/Users/fabianwesner/Herd/shop/app/Models):
- Organization.php
- Store.php
- StoreDomain.php
- StoreUser.php (Pivot; timestamps disabled; creating event auto-fills created_at)
- StoreSettings.php
- User.php (updated with stores() + roleForStore())
- Concerns/BelongsToStore.php
- Scopes/StoreScope.php

Factories (/Users/fabianwesner/Herd/shop/database/factories):
- OrganizationFactory.php
- StoreFactory.php (with suspended() state)
- StoreDomainFactory.php
- StoreSettingsFactory.php
- UserFactory.php (updated with status + last_login_at)

Middleware: /Users/fabianwesner/Herd/shop/app/Http/Middleware/ResolveStore.php (registered as alias `store.resolve` in bootstrap/app.php)

Auth:
- /Users/fabianwesner/Herd/shop/app/Auth/CustomerUserProvider.php (complete UserProvider; scopes to current_store; Customer model referenced lazily)
- config/auth.php updated: `customer` guard, `customers` provider (driver=customer, model=App\Models\Customer), `customers` password broker
- AppServiceProvider boot(): registers `login` rate limiter (5/min per IP) and Auth::provider('customer', ...)

Livewire admin auth:
- /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Auth/Login.php (uses Session facade, not request()->session(), so Livewire tests work without full HTTP context)
- /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/auth/login.blade.php (Flux UI components)
- /Users/fabianwesner/Herd/shop/resources/views/components/layouts/admin-auth.blade.php (minimal centered card with @vite/@livewireStyles/@livewireScripts/@fluxScripts/@fluxAppearance)

Policies:
- /Users/fabianwesner/Herd/shop/app/Policies/Concerns/ChecksStoreRole.php
- /Users/fabianwesner/Herd/shop/app/Policies/StorePolicy.php

Routes: /Users/fabianwesner/Herd/shop/routes/web.php updated with /admin/login and POST /admin/logout (no store.resolve middleware registered on routes yet; alias is registered).

## Key Decisions & Spec Ambiguities

1. **Pivot timestamps on store_users**: Laravel's `AsPivot::fromAttributes` flips `$timestamps = true` automatically when attributes contain `created_at`, even if you statically set `$timestamps = false`. This forced an UPDATED_AT reference on INSERT. Fix: do NOT pass `created_at` through `withPivot`, and set it via a `creating` event on the StoreUser pivot. `withPivot('role')` is the only pivot column now; `created_at` is auto-populated. This is why tests call `$store->users()->attach($user, ['role' => 'owner'])` without a created_at key.

2. **`password_hash` vs `password`**: per your direction I kept Laravel's `password` column. Spec mentions this is the internal convention.

3. **CHECK constraints on SQLite**: Laravel schema builder doesn't emit CHECK constraints for text columns, so I implemented them as BEFORE INSERT (and BEFORE UPDATE for stores.status) triggers that RAISE(ABORT). Covers status, role, type, tls_mode.

4. **Customer model**: not yet created (Phase 6). The customer guard is configured and the provider is registered via `Auth::provider('customer', ...)` in AppServiceProvider so the `App\Models\Customer` class string in config/auth.php is only resolved lazily when the customer guard is actually used. No Customer class = Phase 1 still boots cleanly.

5. **Login component session handling**: Used `Session::regenerate()` / `Session::put()` via facade instead of `request()->session()` because Livewire unit tests don't attach a session to the request. The facade resolves through the app's session manager which does exist in tests.

6. **admin-auth layout**: created new `resources/views/components/layouts/admin-auth.blade.php` rather than reusing the existing `layouts.auth.card`, since the spec said to create it and the existing card layout points at `route('home')` / app branding that doesn't fit an admin login.

7. **StoreScope + BelongsToStore trait**: tested via an in-test `Widget` model that lives only during the test (schema built/dropped in before/after hooks). No real product model uses the trait in Phase 1.

8. **Routes**: `/admin/login` and `/admin/logout` registered. I did NOT attach store.resolve middleware to any route per your instructions (Phase 2).

9. **`.env`**: inspected; all required values were already set (DB_CONNECTION=sqlite, CACHE_STORE=file, SESSION_DRIVER=file, QUEUE_CONNECTION=sync, MAIL_MAILER=log, FILESYSTEM_DISK=local). database.sqlite already existed. No edits needed.

## Blockers
None. Ready for your review.

## Not Committed
I did not create a commit. Staging is yours.
</teammate-message>

<teammate-message teammate_id="phase1-foundation" color="blue">
{"type":"idle_notification","from":"phase1-foundation","timestamp":"2026-04-12T18:27:51.721Z","idleReason":"available"}
</teammate-message>


Phase 1 complete. Let me verify tests and commit.


Bash

php artisan test --compact 2>&1 | tail -30

Verify Phase 1 tests pass


  ..........................................

  Tests:    42 passed (98 assertions)
  Duration: 2.95s


Undefined type 'App\Models\Store'.

Undefined type 'App\Models\StoreDomain'.

Undefined type 'App\Models\StoreUser'.

Undefined type 'App\Models\StoreSettings'.

Symbol '$attributes' is declared but not used.

Expected type 'object'. Found 'TModel'.

Symbol '$exceptions' is declared but not used.

Undefined type 'App\Models\Customer'.

Undefined method 'save'.

Undefined method 'forceFill'.

Undefined method 'stores'.

Undefined method 'forceFill'.

Expected type 'object'. Found 'null'.

Expected type 'object'. Found 'null'.

Undefined method 'stores'.

Undefined method 'forceFill'.

Expected type 'object'. Found 'null'.

Expected type 'object'. Found 'null'.

Symbol 'App\Livewire\Admin\Auth\Session' is declared but not used.

Undefined method 'get'.

Undefined method 'get'.

Undefined method 'get'.

Undefined method 'get'.

Symbol '$user' is declared but not used.


All 42 tests pass. Diagnostic warnings are mostly intelephense false positives (models exist). Let me commit and move to Phase 2.


Check git status


 M app/Models/User.php
 M app/Providers/AppServiceProvider.php
 M bootstrap/app.php
 M config/auth.php
 M database/factories/UserFactory.php
 M routes/web.php
?? app/Auth/
?? app/Enums/
?? app/Http/Middleware/
?? app/Livewire/Admin/
?? app/Models/Concerns/
?? app/Models/Organization.php
?? app/Models/Scopes/
?? app/Models/Store.php
?? app/Models/StoreDomain.php
?? app/Models/StoreSettings.php
?? app/Models/StoreUser.php
?? app/Policies/
?? database/factories/OrganizationFactory.php
?? database/factories/StoreDomainFactory.php
?? database/factories/StoreFactory.php
?? database/factories/StoreSettingsFactory.php
?? database/migrations/2026_04_12_100001_create_organizations_table.php
?? database/migrations/2026_04_12_100002_create_stores_table.php
?? database/migrations/2026_04_12_100003_create_store_domains_table.php
?? database/migrations/2026_04_12_100004_modify_users_table_for_shop.php
?? database/migrations/2026_04_12_100005_create_store_users_table.php
?? database/migrations/2026_04_12_100006_create_store_settings_table.php
?? resources/views/components/layouts/
?? resources/views/livewire/admin/
?? specs/progress.md
?? tests/Feature/Auth/AdminAuthTest.php
?? tests/Feature/Tenancy/


Full git status