================================================================================ LIQUOR ECOMMERCE PLATFORM — ARCHITECTURE V1.2 COMPLETE DUMP Generated for ChatGPT / external review Project: D:\xampp\htdocs\liquor-ecommerce Stack: Laravel 13, Next.js 16 (storefront+admin), MySQL 8, Redis, Typesense IMPORTANT: Documentation only. NO migrations/models/controllers/APIs implemented yet. Implementation readiness: READY for core Phase-1 (48 REQUIRED tables) with feature gates. ================================================================================ ################################################################################ # SECTION 0 — EXECUTIVE SUMMARY OF WHAT WAS DONE ################################################################################ WORK COMPLETED ACROSS SESSIONS: 1. Inspected repo: Laravel 13 skeleton only (users/cache/jobs starter migrations). No domain code. 2. Created Architecture v1.0 from docs/first-prompt.txt (modular monolith design docs). 3. Architecture review feedback in docs/architecture-review.txt → Architecture v1.1 (IDs, money, inventory, state machines, idempotency, outbox, ADRs). 4. Final cross-document + PRD consistency review → Architecture v1.2 (contradictions fixed, matrices, table registry, readiness). VERSION HISTORY: - v1.0: Initial architecture analysis and module boundaries - v1.1: Foundational DB/transaction decisions + ADRs 001-009 - v1.2: Consistency pass, ownership fixes, SALE commit point, payment retry, schema minimalism, matrices KEY v1.2 FIXES: - Store owns inventory_locations (Inventory owns quantities only) - SALE ledger commit ONLY at fulfillment ALLOCATED (NOT at payment capture) - Failed payment attempt does NOT auto-cancel order - Order COMPLETED is quantity/outcome based (not all fulfillments terminal) - Do NOT migrate speculative foundation tables (48 migrate now only) - Pre-arrival: incoming_inventory_reservations (Option A) - Fulfillment source: typed FKs (location_id XOR vendor_id) - Pickup: READY_FOR_PICKUP / PICKED_UP on fulfillment (no fake shipment) - Outbox PROCESSED = successful Redis enqueue - Event IDs: ULID event_id; external webhooks use public ULIDs only - Laravel 13 locked; checkout outer TX for reserve+order+outbox; providers outside TX TABLE COUNTS (canonical TABLE_REGISTRY.md): - REQUIRED NOW (migrate): 48 - ARCHITECTURAL FOUNDATION (design only, no migrate): 36 - DEFERRED: 10 - Total designed: 94 IMPLEMENTATION STATUS: - NO migrations created for domain - NO models/controllers/APIs/packages/business logic - Docs only under docs/architecture/ OPEN BUSINESS GATES (do not invent; see section 21): B1 pre-arrival selling, B2 pickup at launch, B3 guest checkout, B4 compliance rules, B5 age verification, B6 promotions, B7 collections; plus C1-C10 configurable confirms. ================================================================================ FILE: docs/architecture/README.md ================================================================================ ## Architecture v1.2 (Documentation-Only) **Status:** Architecture v1.2 — consistency + PRD alignment pass. **Implementation:** NOT started. Migrations/models/APIs NOT created. ### Version History | Version | Notes | |---|---| | v1.0 | Initial architecture | | v1.1 | Foundational DB/transaction decisions | | **v1.2** | Cross-document consistency, ownership, commit points, table registry, matrices | ### Start Here 1. `ARCHITECTURE_REVIEW.md` 2. `PRD_TRACEABILITY_MATRIX.md` 3. `TABLE_REGISTRY.md` 4. `MODULE_OWNERSHIP_MATRIX.md` 5. `ARCHITECTURE_CONSISTENCY_MATRIX.md` 6. `21-open-decisions.md` ### New v1.2 Matrices - PRD_TRACEABILITY_MATRIX.md - MODULE_OWNERSHIP_MATRIX.md - TABLE_REGISTRY.md - ARCHITECTURE_CONSISTENCY_MATRIX.md - SEARCH_PROJECTION_MATRIX.md - CRITICAL_SCENARIO_TEST_MATRIX.md ### Baseline Laravel 13 modular monolith; MySQL canonical; Redis queues; Typesense projection. ================================================================================ FILE: docs/architecture/ARCHITECTURE_REVIEW.md ================================================================================ ## ARCHITECTURE_REVIEW.md — v1.2 ### Executive Summary Architecture v1.2 makes v1.1 **internally consistent** and aligned with the architecture PRD (`first-prompt.txt`). Critical contradictions (ownership, SALE commit, payment retry, speculative migrations, order completion, outbox semantics, event IDs) are resolved. Speculative foundation table migration is rejected. **Implementation readiness: READY for core Phase-1 commerce path** (with feature gates for pre-arrival, pickup UX confirmation, promotions, compliance rule content, guest checkout confirmation). ### Decisions Changed from v1.1 1. Store owns `inventory_locations` 2. SALE/commit at fulfillment ALLOCATED only 3. Payment attempt failure ≠ order cancel 4. Order completion by quantities 5. Do not migrate 28 foundation tables 6. Pre-arrival uses `incoming_inventory_reservations` 7. Typed fulfillment source FKs 8. Pickup states on fulfillment 9. Outbox PROCESSED = Redis enqueue success 10. Event ID rules for internal vs external 11. Audit PII allowlists + retention lifecycle 12. Cursor totals optional 13. Laravel 13 locked in architecture docs 14. ADR-010 checkout TX; ADR-011 schema minimalism ### Final Module Ownership See MODULE_OWNERSHIP_MATRIX.md. ### Final Table Counts 48 REQUIRED migrate | 36 foundation schema-less | 10 deferred. ### Risks Remaining - High concurrency inventory under load (mitigated by tests) - Compliance content unknown (feature content, not structure) - Provider indeterminate outcomes (reconcile jobs required) - Pre-arrival partial receiving operational complexity when enabled ### Approval Gate - [x] Technical contradictions resolved - [ ] Client confirms feature gates in `21-open-decisions.md` as needed - [ ] Explicit go-ahead to implement migrations for 48 REQUIRED tables ================================================================================ FILE: docs/architecture/PRD_TRACEABILITY_MATRIX.md ================================================================================ # PRD Traceability Matrix — Architecture v1.2 **Baseline sources:** `docs/first-prompt.txt` (architecture PRD / first-principles requirements), Architecture v1.1 docs, `docs/architecture-review.txt`. **Note:** No separate client marketing PRD file exists in the repository. This matrix traces the approved architecture prompt + review instructions. Where Phase-1 feature inclusion is not explicitly mandated, status reflects architectural coverage vs open business confirmation. **Status legend:** COVERED | PARTIALLY COVERED | NOT COVERED | CONFLICTING --- ## A. Catalog & Merchandising | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Products ≠ inventory/price/vendor product | P1 | Critical | Catalog | Pricing, Inventory, Vendors | products, product_variants | Catalog DTOs | product.* | — | 04, ADR-008 | P1 | COVERED | | Product variants as sellable unit | P1 | Critical | Catalog | Cart, Pricing, Inventory, Orders | product_variants | CartItem→Variant | — | — | 04, O rule | P1 | COVERED | | Categories | P1 | High | Catalog | Search, SEO | categories, product_categories | Catalog list/filter | product.updated | — | 04 | P1 | COVERED | | Brands | P1 | High | Catalog | Search, SEO | brands | Catalog filter | product.updated | — | 04 | P1 | COVERED | | Collections | Foundation | Medium | Catalog | CMS, Promotions | collections, collection_products | Admin CMS | — | — | 04, 17 | Schema when needed | PARTIALLY COVERED | | Configurable + dedicated attributes | P1 | High | Catalog | Search | product_variants cols + product_attributes* | Filter APIs | product.updated | — | 04 (v1.2 freeze) | P1 | COVERED | | Media | P1 | High | Media/Catalog | CMS | media_assets, catalog_media | Media upload | — | StorageProvider | 04, 17 | P1 | COVERED | | Advanced filters (type, brand, region, country, varietal, price, promo) | P1 | High | Search + Catalog | Pricing, Promotions | Typesense projection | GET /search | product.*, price.*, inventory.* | Typesense | 16, SEARCH_MATRIX | P1 | PARTIALLY COVERED | | Sorting | P1 | High | Search | Catalog | Typesense | GET /search | — | Typesense | 16 | P1 | COVERED | | Bestseller/trending capability | Future | Low | Search | Orders analytics | — | Search sort modes | — | — | 16 | Deferred | NOT COVERED | | Slug/SEO URLs for products/categories/brands | P1 | High | Catalog + SEO | CMS | slug cols, seo_metadata, redirects | Storefront routes | — | — | 17, AG | P1 | COVERED | ## B. Inventory & Availability | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Manual inventory (ledger + levels) | P1 | Critical | Inventory | Store | inventory_levels, inventory_transactions | Admin adjust/receive | inventory.adjusted | — | 05, ADR-003 | P1 | COVERED | | Multi-store inventory foundation | Foundation | Critical | Store + Inventory | Serviceability | stores, inventory_locations, inventory_levels | Availability API | inventory.* | — | 25 | Schema: stores+locations P1 | COVERED | | Location-aware availability | P1 | Critical | Inventory + Location | Compliance, Shipping | inventory_levels, service_zones* | GET /availability | inventory.* | — | 25 | P1 | COVERED | | Nearby store visibility | Foundation | Medium | Location | Inventory | stores, addresses, service_zones | Availability ranking | — | — | 25 (SERVICEABILITY vs RANKING) | Ranking P1 display optional | PARTIALLY COVERED | | Pickup availability by store | Open→P1 if confirmed | High | Fulfillment + Store | Compliance | fulfillments.method | Checkout options | fulfillment.* | — | 22, 07 | Blocks pickup feature if undecided | CONFLICTING→RESOLVED as open w/ design ready | | Pre-arrival inventory model | Foundation / feature open | High | Inventory | Orders | incoming_inventory, incoming_inventory_reservations | Admin + checkout | incoming.* | — | 24 | Schema when selling pre-arrival | PARTIALLY COVERED | | Inventory transfers | Deferred | Low | Inventory | Store | inventory_transfers* | Admin | — | — | 05 | Future | NOT COVERED (by design) | | POS readiness (SYNC_CORRECTION) | Deferred | Medium | Inventory | Integration | inventory_transactions type | Integration write | inventory.adjusted | POS adapter | 05, 10 | Future | PARTIALLY COVERED | | Oversell prevention | P1 | Critical | Inventory | Checkout | inventory_reservations | ReserveAction | inventory.reservation.* | — | 05, 23 | P1 | COVERED | ## C. Vendors & Integrations | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Vendor architecture (not SGProof-centric) | Foundation | Critical | Vendors | Integration | vendors* | VendorConnector + capabilities | vendor.sync.* | VendorConnector | 10, 24, INTEGRATION_CONTRACTS | Contracts P1; schema later | COVERED | | Future SGProof (browser automation) | Deferred integration | High | Vendors | Integration | vendor_* | SGProofConnector | vendor.sync.* | Playwright | 10 | Separate approved phase | COVERED (foundation) | | Future vendor APIs / CSV | Deferred | Medium | Vendors | Integration | vendor_* | Api/Csv connectors | vendor.sync.* | — | 10 | Future | COVERED (foundation) | | Vendor cost ≠ retail | P1 design | Critical | Vendors + Pricing | Checkout | vendor_offers vs prices | PricingEngine | — | — | 24, 06 | P1 | COVERED | | Vendor availability not ledger guarantee | P1 design | Critical | Vendors | Fulfillment | vendor_offers | Capability contracts | — | — | 24 | P1 | COVERED | | Integration registry + encrypted credentials | Foundation | High | Integration | All adapters | integrations* | Integration registry | — | — | 14 | Schema when first adapter | COVERED | | Provider independence | P1 | Critical | Integration | All | — | Contracts | — | ADR-006 | INTEGRATION_CONTRACTS | P1 | COVERED | ## D. Pricing, Promotions, Tax | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Independent pricing engine | P1 | Critical | Pricing | Catalog, Store | price_lists, prices | PricingEngine | price.updated | — | 06, ADR-004 | P1 | COVERED | | Coupons / promotions extensible | Foundation | Medium | Promotions | Checkout | promotions*, coupons* | PromotionEngine | — | — | 06 | Schema when first promo | PARTIALLY COVERED | | Free-shipping eligibility | Foundation | Medium | Promotions + Shipping | Checkout | promotion_actions | Checkout quote | — | — | 06, 09 | With promotions | PARTIALLY COVERED | | Tax | P1 | High | Tax (via Pricing/Checkout) | Compliance | order tax snapshot fields | TaxProvider | — | TaxProvider | 06, INTEGRATION | P1 | PARTIALLY COVERED | | Wholesale roadmap | Deferred | Low | Pricing | — | — | — | — | — | 06 | Future | NOT COVERED (by design) | ## E. Cart, Checkout, Orders, Payments | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Cart | P1 | Critical | Cart | Catalog | carts, cart_items | Cart APIs | — | — | Cart docs | P1 | COVERED | | Checkout orchestration (not god service) | P1 | Critical | Checkout (app) | Orders, Inventory, Pricing, Compliance, Payment | — | PlaceOrderCommand | order.placed | — | CONSISTENCY_MATRIX | P1 | COVERED | | Guest checkout | Open | High | Cart/Identity | Orders | carts.customer_id nullable | Checkout | — | — | 21 | Schema supports guest | NOT COVERED (open) | | Orders with snapshots | P1 | Critical | Orders | Catalog, Pricing | orders, order_items, order_addresses, order_status_history | Order APIs | order.* | — | 07, 22 | P1 | COVERED | | Payment ≠ order | P1 | Critical | Payments | Orders | payments, attempts, transactions, refunds | PaymentGateway | payment.* | PaymentGateway | 08, ADR-008 | P1 | COVERED | | Payment retry without auto-cancel | P1 | Critical | Payments + Orders | Inventory | payment_attempts | Retry payment | payment.failed | — | 08, 22 | P1 | COVERED (v1.2 fix) | | Idempotent payments/refunds/orders | P1 | Critical | Shared | All write modules | idempotency_keys | Idempotency-Key | — | — | 23 | P1 | COVERED | | Auth: customer / admin / integration | P1 | Critical | Identity | All | users, customers, roles* | Auth contracts | customer.registered | — | 15 | P1 | COVERED | | Admin RBAC | P1 | Critical | Identity | Audit | roles, permissions | Admin APIs | — | — | 15 | P1 | COVERED | ## F. Fulfillment, Shipping, Pickup | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Partial / multi-source fulfillment | Foundation→P1 capable | High | Fulfillment | Inventory, Vendors | fulfillments, fulfillment_items | Fulfillment APIs | fulfillment.* | — | 07, 22 | P1 single-source OK | COVERED | | Multiple shipments | Foundation | High | Fulfillment + Shipping | — | shipments, shipment_items, tracking | ShippingProvider | shipment.* | ShippingProvider | 07, 09 | P1 | COVERED | | Future delivery partners | Foundation | Critical | Shipping | Integration | shipping_methods, providers* | Capability contracts | — | Shipping* | 09, ADR-006 | Contracts P1 | COVERED | | Rapid shipping roadmap | Deferred | Low | Shipping | — | — | — | — | — | 09 | Future | NOT COVERED (by design) | | Pickup lifecycle | Open / design ready | High | Fulfillment | Store | fulfillments (method=PICKUP) | Pickup complete API | fulfillment.picked_up | — | 22 | Feature gated | PARTIALLY COVERED | | Order ≠ shipment | P1 | Critical | Orders / Fulfillment / Shipping | — | separate aggregates | — | — | — | ADR-008 | P1 | COVERED | ## G. Compliance, CMS, SEO, Search, Audit, Notifications | PRD Requirement | Phase | Priority | Owning Module | Supporting | Tables | API/Contracts | Events | Provider | Arch Doc | Impl Phase | Status | |---|---|---|---|---|---|---|---|---|---|---|---| | Extensible compliance engine | Foundation | Critical | Compliance | Checkout, Fulfillment | compliance_rules, jurisdictions | ComplianceDecision | — | AgeVerification* | 11 | Schema when rules exist | COVERED | | CMS homepage configurable | P1 | High | CMS | Media | pages, content_sections, site_settings | Admin CMS | — | — | 17 | P1 | COVERED | | SEO metadata + redirects | P1 | High | SEO | Catalog, CMS | seo_metadata, redirects | SEO APIs | — | — | 17 | P1 | COVERED | | Search as projection (Typesense) | P1 | Critical | Search | Catalog, Pricing, Inventory | search_index_state | SearchProvider | * → index | Typesense | 16, ADR-007 | P1 | COVERED | | Search never transactional SoT | P1 | Critical | Search | Checkout | — | — | — | — | 16 | P1 | COVERED | | Audit logging | P1 | Critical | Audit | All admin | audit_logs | — | — | — | 18 | P1 | COVERED | | Notifications | Foundation | Medium | Notification | Orders, Payments | — | NotificationProvider | order.*, payment.* | Email/SMS | INTEGRATION | Async P1 minimal | PARTIALLY COVERED | | API versioning / OpenAPI | P1 | Critical | Shared | All | — | /api/v1 | — | — | ADR-009 | P1 | COVERED | | Transactional outbox | P1 | Critical | Shared | All | outbox_messages | — | all domain events | — | 13, ADR-005 | P1 | COVERED | | Laravel 13 baseline | P1 | Critical | Platform | — | — | — | — | — | README, 20 | P1 | COVERED | | AI/MCP readiness (not implement) | Future | Low | API | — | — | Stable APIs | webhooks | — | first-prompt | Future | COVERED (design) | --- ## Conflicts Found vs Architecture v1.1 | Conflict | Severity | v1.2 Resolution | |---|---|---| | `inventory_locations` dual ownership (Store vs Inventory) | High | Store owns; Inventory references | | SALE commit on payment vs fulfillment allocation | Critical | Single commit at fulfillment ALLOCATED | | Payment FAILED auto-cancels order | High | Failed attempt ≠ cancel; expiry/abandon cancels | | Order COMPLETED when all fulfillments terminal (includes CANCELLED) | High | Quantity/outcome-based completion | | 28 FOUNDATION tables migrated speculatively | High | Migrate only REQUIRED; foundation = design/contracts | | Collections/attributes FOUNDATION while filters need attributes | Medium | Dedicated variant filter columns REQUIRED; EAV schema when needed | | Pre-arrival FOUNDATION while PRD models pre-arrival deeply | Medium | Design complete; migrate when client confirms selling | | Pickup open but PRD requires pickup availability architecture | Medium | Lifecycle designed; feature gated on client confirmation | | EVENT_CATALOG exposes internal BIGINT IDs | Medium | Event ID strategy split (internal vs external) | | Fulfillment owns shipments while Shipping owns shipping | Medium | Fulfillment owns shipment aggregates; Shipping owns provider orchestration | | Laravel version drift risk | Low | Lock Laravel 13 in architecture docs | --- ## Coverage Summary | Status | Count (approx) | |---|---| | COVERED | Majority of critical P1 commerce path | | PARTIALLY COVERED | Collections, promotions, nearby ranking, pickup, pre-arrival selling, tax provider, notifications | | NOT COVERED (deferred by design) | Bestseller analytics, transfers, wholesale, rapid shipping, SGProof implementation | | CONFLICTING | Resolved in v1.2 (see above) | ================================================================================ FILE: docs/architecture/MODULE_OWNERSHIP_MATRIX.md ================================================================================ # Module Ownership Matrix — Architecture v1.2 **Rule:** Every table has exactly **one** canonical owning module. Other modules may **read** via contracts/queries; they **mutate** only through the owner’s public actions. **FK policy:** Cross-module FKs reference internal BIGINT PKs of the owned table. The referencing module never updates the owned table’s non-FK columns. --- ## Ownership Resolution (v1.1 Conflicts Fixed) | Conflict | v1.2 Canonical Owner | |---|---| | `inventory_locations` | **Store** (location identity). Inventory references `inventory_location_id`. | | `shipments` / tracking | **Fulfillment** owns shipment aggregates. **Shipping** owns provider config + orchestration adapters. | | Checkout | **Application orchestrator** — owns no commerce state tables. | | `outbox_messages` | **Shared Infrastructure** (Platform), written via OutboxPublisher helper used by all modules. | | `idempotency_keys` | **Shared Infrastructure** (Platform). | | `addresses` (operational) | **Identity/Store** for customer/store addresses; **Orders** owns immutable `order_addresses` snapshots. | --- ## Matrix | Table/Aggregate | Owning Module | Read By | Mutated Through | Public Contract | Events Published | Cross-module FK Policy | |---|---|---|---|---|---|---| | `users` | Identity | Admin, Audit | Identity actions | Auth/UserService | — | FK from audit actor refs (soft) | | `customers` | Identity | Orders, Cart, Checkout | Identity actions | CustomerService | customer.registered | Orders FK customer_id | | `roles`, `permissions`, `role_permissions`, `user_roles` | Identity | Admin middleware | Identity RBAC actions | AuthorizationService | — | Internal | | `products` | Catalog | Search, CMS, Admin | Catalog actions | CatalogProductService | product.created/updated/deleted | Soft-delete; historical FKs RESTRICT | | `product_variants` | Catalog | Inventory, Pricing, Cart, Orders, Vendors, Search | Catalog actions | CatalogVariantService | product.updated | Sellable unit FK target | | `brands`, `categories`, `product_categories` | Catalog | Search, SEO | Catalog actions | CatalogTaxonomyService | product.updated | — | | `collections`, `collection_products` | Catalog | CMS, Promotions | Catalog actions | CollectionService | — | Schema when needed | | `product_attributes`, `product_attribute_values` | Catalog | Search | Catalog actions | AttributeService | product.updated | Schema when needed | | `media_assets`, `catalog_media` | Media (Catalog-adjacent) | Catalog, CMS | Media actions | StorageProvider + MediaService | — | — | | `seo_metadata`, `redirects` | SEO | Storefront | SEO actions | SeoService | — | Polymorphic attachable | | `pages`, `content_sections`, `site_settings` | CMS | Storefront | CMS actions | CmsService | — | — | | `menus`, `menu_items`, `banners` | CMS | Storefront | CMS actions | CmsService | — | Schema when needed | | `stores` | Store | Inventory, Orders, Availability | Store actions | StoreService | — | — | | `store_addresses` | Store | Availability, Shipping | Store actions | StoreService | — | — | | `inventory_locations` | **Store** | Inventory, Fulfillment, Availability | Store actions | LocationService | — | Inventory FK location_id | | `service_zones`, `service_zone_stores`, `serviceability_rules` | Store/Location | Checkout, Shipping, Compliance | Location actions | ServiceabilityService | — | Schema when multi-zone | | `inventory_levels` | **Inventory** | Availability, Search, Checkout | Inventory actions only | InventoryLevelService | inventory.adjusted | UNIQUE(location, variant) | | `inventory_transactions` | Inventory | Admin, Audit | Inventory actions only | InventoryLedgerService | inventory.sale/adjusted | Append-only | | `inventory_reservations` | Inventory | Orders, Checkout | Inventory actions only | ReservationService | inventory.reservation.* | — | | `incoming_inventory` | Inventory | Checkout, Admin | Inventory actions | IncomingInventoryService | incoming.* | — | | `incoming_inventory_reservations` | Inventory | Orders, Checkout | Inventory actions | PreArrivalReservationService | incoming.reservation.* | — | | `price_lists`, `prices` | Pricing | Checkout, Search, Cart | Pricing actions | PricingEngine | price.updated | Variant + optional store | | `promotions`, `promotion_rules`, `promotion_actions`, `coupons`, `promotion_redemptions` | Promotions | Checkout | Promotion actions | PromotionEngine | — | Schema when needed | | `carts`, `cart_items` | Cart | Checkout | Cart actions | CartService | — | customer_id nullable (guest-ready) | | `orders`, `order_items`, `order_addresses`, `order_status_history` | **Orders** | Admin, Fulfillment, Payments (read) | Order actions only | OrderService | order.* | Snapshots; RESTRICT deletes | | `payments`, `payment_attempts`, `payment_transactions`, `refunds`, `payment_webhook_events` | **Payments** | Orders (read status) | Payment actions only | PaymentService / PaymentGateway | payment.* | order_id FK | | `fulfillments`, `fulfillment_items`, `fulfillment_status_history` | **Fulfillment** | Orders, Shipping, Admin | Fulfillment actions | FulfillmentService | fulfillment.* | Typed FKs location/vendor | | `shipments`, `shipment_items`, `shipment_tracking_events` | **Fulfillment** | Shipping adapters (write via Fulfillment), Notifications | Fulfillment + Shipping orchestration | ShipmentService | shipment.* | Shipping never owns rows | | `shipping_methods`, `shipping_zones`, `shipping_rates`, `shipping_providers` | **Shipping** | Checkout, Fulfillment | Shipping admin actions | ShippingRateProvider / ShippingProvider | — | Config only | | `vendors`, `vendor_accounts`, `vendor_products`, `vendor_offers`, `vendor_sync_*` | Vendors | Search, Fulfillment, Pricing (cost) | Vendor sync actions | VendorConnector | vendor.sync.* | Never merge into inventory_levels | | `compliance_rules`, `jurisdictions` | Compliance | Checkout, Fulfillment | Compliance admin | ComplianceEngine | — | Schema when rules exist | | `integrations`, `integration_credentials`, `integration_settings`, `integration_logs` | Integration | Adapters | Integration admin | IntegrationRegistry | — | Encrypted credentials | | `webhook_endpoints`, `webhook_subscriptions`, `webhook_deliveries` | Integration | Outbox consumers | Webhook delivery jobs | WebhookDispatcher | — | External IDs only | | `audit_logs` | Audit | Admin | AuditWriter (append-only) | AuditService | — | Soft polymorphic refs, no hard FK | | `outbox_messages` | Platform | Outbox worker | OutboxPublisher | OutboxPublisher | (payloads) | Written in same TX as domain | | `idempotency_keys` | Platform | Middleware | Idempotency middleware | IdempotencyStore | — | — | | `search_index_state` | Search | Admin | SearchIndexJob | SearchProvider | — | Projection only | --- ## Checkout Orchestrator (Non-Owner) Checkout **does not own** tables. It invokes, within a defined transaction boundary (see CONSISTENCY_MATRIX / system overview): 1. PricingEngine.quote 2. PromotionEngine.apply (optional) 3. ComplianceEngine.evaluate 4. Inventory.ReservationService.reserve (sync, must succeed) 5. Orders.OrderService.createPendingPayment (sync) 6. OutboxPublisher.write (`order.placed`) 7. After commit: Payments initiate (external network **outside** DB TX) --- ## Module Dependency Rules Allowed: Checkout → module contracts; Consumers → events. Forbidden: - Checkout/Orders directly UPDATE `inventory_levels` - Fulfillment directly UPDATE `payments` - Shipping SDK usage inside Orders - Inventory UPDATE `inventory_locations` rows (except reading FK) ================================================================================ FILE: docs/architecture/TABLE_REGISTRY.md ================================================================================ # TABLE_REGISTRY.md — Architecture v1.2 (Canonical) **All table counts in architecture docs MUST derive from this registry.** ### Classification (v1.2) | Label | Meaning | Migrate now? | |---|---|---| | **REQUIRED NOW** | Needed for Phase-1 MVP path | YES | | **ARCHITECTURAL FOUNDATION** | Designed/contracts now; **no schema until feature approved** | NO (unless Exception) | | **DEFERRED** | No design urgency | NO | **Exception rule:** Migrate a foundation table now only if postponing requires **destructive redesign** of a REQUIRED table. Document each exception below. --- ## Registry | Table | Module Owner | Classification | Phase | Migration Now? | public_id? | Soft Delete? | Primary FKs | Purpose | |---|---|---|---|---|---|---|---|---| | users | Identity | REQUIRED NOW | P1 | YES | YES | NO | — | Admin/staff principals | | customers | Identity | REQUIRED NOW | P1 | YES | YES | NO | — | Storefront principals | | roles | Identity | REQUIRED NOW | P1 | YES | NO | NO | — | RBAC | | permissions | Identity | REQUIRED NOW | P1 | YES | NO | NO | — | RBAC | | role_permissions | Identity | REQUIRED NOW | P1 | YES | NO | NO | role, permission | RBAC pivot | | user_roles | Identity | REQUIRED NOW | P1 | YES | NO | NO | user, role | RBAC pivot | | products | Catalog | REQUIRED NOW | P1 | YES | YES | YES | brand_id? | Merchandising entity | | product_variants | Catalog | REQUIRED NOW | P1 | YES | YES | YES | product_id | Sellable unit | | brands | Catalog | REQUIRED NOW | P1 | YES | YES | YES | — | Brand | | categories | Catalog | REQUIRED NOW | P1 | YES | YES | YES | parent_id? | Taxonomy | | product_categories | Catalog | REQUIRED NOW | P1 | YES | NO | NO | product, category | Pivot | | media_assets | Media | REQUIRED NOW | P1 | YES | YES | YES | — | Stored files | | catalog_media | Media | REQUIRED NOW | P1 | YES | NO | NO | variant/product, media | Product media links | | seo_metadata | SEO | REQUIRED NOW | P1 | YES | NO | NO | attachable polymorphic | SEO | | redirects | SEO | REQUIRED NOW | P1 | YES | NO | NO | — | Slug/URL redirects | | pages | CMS | REQUIRED NOW | P1 | YES | YES | YES | — | CMS pages | | content_sections | CMS | REQUIRED NOW | P1 | YES | NO | NO | page_id | Homepage sections | | site_settings | CMS | REQUIRED NOW | P1 | YES | NO | NO | — | Key/value settings | | stores | Store | REQUIRED NOW | P1 | YES | YES | NO | — | Store entity (supports multi later) | | store_addresses | Store | REQUIRED NOW | P1 | YES | NO | NO | store_id | Store address | | inventory_locations | Store | REQUIRED NOW | P1 | YES | YES | NO | store_id nullable | Stock location identity | | inventory_levels | Inventory | REQUIRED NOW | P1 | YES | NO | NO | location_id, variant_id | on_hand/reserved | | inventory_transactions | Inventory | REQUIRED NOW | P1 | YES | NO | NO | level_id | Physical ledger | | inventory_reservations | Inventory | REQUIRED NOW | P1 | YES | NO | NO | level_id, order_id? | Physical reservations | | price_lists | Pricing | REQUIRED NOW | P1 | YES | YES | NO | — | Price list | | prices | Pricing | REQUIRED NOW | P1 | YES | NO | NO | list_id, variant_id, store_id? | Retail amounts | | carts | Cart | REQUIRED NOW | P1 | YES | NO | NO | customer_id nullable | Cart header | | cart_items | Cart | REQUIRED NOW | P1 | YES | NO | NO | cart_id, variant_id | Cart lines | | orders | Orders | REQUIRED NOW | P1 | YES | YES | NO | customer_id?, store_id | Order header | | order_items | Orders | REQUIRED NOW | P1 | YES | NO | NO | order_id, variant_id | Snapshot lines | | order_addresses | Orders | REQUIRED NOW | P1 | YES | NO | NO | order_id | Address snapshots | | order_status_history | Orders | REQUIRED NOW | P1 | YES | NO | NO | order_id | Transitions | | payments | Payments | REQUIRED NOW | P1 | YES | YES | NO | order_id | Payment aggregate | | payment_attempts | Payments | REQUIRED NOW | P1 | YES | NO | NO | payment_id | Immutable attempts | | payment_transactions | Payments | REQUIRED NOW | P1 | YES | NO | NO | attempt_id | Append-only txs | | refunds | Payments | REQUIRED NOW | P1 | YES | YES | NO | payment_id | Append-only refunds | | payment_webhook_events | Payments | REQUIRED NOW | P1 | YES | NO | NO | — | Webhook dedup | | fulfillments | Fulfillment | REQUIRED NOW | P1 | YES | YES | NO | order_id, location_id?, vendor_id? | Fulfillment | | fulfillment_items | Fulfillment | REQUIRED NOW | P1 | YES | NO | NO | fulfillment_id, order_item_id | Allocations | | fulfillment_status_history | Fulfillment | REQUIRED NOW | P1 | YES | NO | NO | fulfillment_id | Transitions | | shipments | Fulfillment | REQUIRED NOW | P1 | YES | YES | NO | fulfillment_id | Shipment (ship method only) | | shipment_items | Fulfillment | REQUIRED NOW | P1 | YES | NO | NO | shipment_id | Shipment lines | | shipment_tracking_events | Fulfillment | REQUIRED NOW | P1 | YES | NO | NO | shipment_id | Tracking | | shipping_methods | Shipping | REQUIRED NOW | P1 | YES | YES | NO | — | Delivery/pickup methods | | audit_logs | Audit | REQUIRED NOW | P1 | YES | NO | NO | soft refs | Append-only audit | | outbox_messages | Platform | REQUIRED NOW | P1 | YES | NO (event_id ULID) | NO | — | Transactional outbox | | idempotency_keys | Platform | REQUIRED NOW | P1 | YES | NO | NO | — | Write idempotency | | search_index_state | Search | REQUIRED NOW | P1 | YES | NO | NO | — | Index lag tracking | | collections | Catalog | ARCHITECTURAL FOUNDATION | Later | NO | YES | YES | — | Merchandising collections | | collection_products | Catalog | ARCHITECTURAL FOUNDATION | Later | NO | NO | NO | collection, product | Pivot | | product_attributes | Catalog | ARCHITECTURAL FOUNDATION | Later | NO | NO | NO | — | EAV schema (beyond dedicated cols) | | product_attribute_values | Catalog | ARCHITECTURAL FOUNDATION | Later | NO | NO | NO | attribute, variant | EAV values | | service_zones | Store | ARCHITECTURAL FOUNDATION | Multi-zone | NO | YES | NO | — | Geography eligibility | | service_zone_stores | Store | ARCHITECTURAL FOUNDATION | Multi-zone | NO | NO | NO | zone, store | Pivot | | serviceability_rules | Store | ARCHITECTURAL FOUNDATION | Multi-zone | NO | NO | NO | zone_id | Rules | | incoming_inventory | Inventory | ARCHITECTURAL FOUNDATION | Pre-arrival | NO* | YES | NO | variant, location, vendor? | Incoming stock | | incoming_inventory_reservations | Inventory | ARCHITECTURAL FOUNDATION | Pre-arrival | NO* | NO | NO | incoming, order | Pre-arrival holds | | promotions | Promotions | ARCHITECTURAL FOUNDATION | Promo launch | NO | YES | NO | — | Promotions | | promotion_rules | Promotions | ARCHITECTURAL FOUNDATION | Promo launch | NO | NO | NO | promotion_id | Rules | | promotion_actions | Promotions | ARCHITECTURAL FOUNDATION | Promo launch | NO | NO | NO | promotion_id | Actions | | coupons | Promotions | ARCHITECTURAL FOUNDATION | Promo launch | NO | YES | NO | promotion_id | Coupon codes | | promotion_redemptions | Promotions | ARCHITECTURAL FOUNDATION | Promo launch | NO | NO | NO | coupon, order | Redemptions | | shipping_zones | Shipping | ARCHITECTURAL FOUNDATION | Rate engine | NO | YES | NO | — | Rate geography | | shipping_rates | Shipping | ARCHITECTURAL FOUNDATION | Rate engine | NO | NO | NO | method, zone | Rates | | shipping_providers | Shipping | ARCHITECTURAL FOUNDATION | Multi-carrier | NO | YES | NO | — | Provider registry | | vendors | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | YES | NO | — | Vendor registry | | vendor_accounts | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | YES | NO | vendor_id | Accounts | | vendor_products | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | NO | NO | account, variant? | Mapping | | vendor_offers | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | NO | NO | vendor_product, variant | Cost/avail sync | | vendor_sync_runs | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | NO | NO | account | Sync runs | | vendor_sync_errors | Vendors | ARCHITECTURAL FOUNDATION | Vendor phase | NO | NO | NO | sync_run | Errors | | compliance_rules | Compliance | ARCHITECTURAL FOUNDATION | Compliance | NO | NO | NO | jurisdiction? | Rules | | jurisdictions | Compliance | ARCHITECTURAL FOUNDATION | Compliance | NO | YES | NO | — | Jurisdictions | | integrations | Integration | ARCHITECTURAL FOUNDATION | First adapter | NO | YES | NO | — | Registry | | integration_credentials | Integration | ARCHITECTURAL FOUNDATION | First adapter | NO | NO | NO | integration_id | Encrypted secrets | | integration_settings | Integration | ARCHITECTURAL FOUNDATION | First adapter | NO | NO | NO | integration_id | Config | | integration_logs | Integration | ARCHITECTURAL FOUNDATION | First adapter | NO | NO | NO | integration_id | Logs | | webhook_endpoints | Integration | ARCHITECTURAL FOUNDATION | Outbound webhooks | NO | YES | NO | — | Endpoints | | webhook_subscriptions | Integration | ARCHITECTURAL FOUNDATION | Outbound webhooks | NO | NO | NO | endpoint | Subscriptions | | webhook_deliveries | Integration | ARCHITECTURAL FOUNDATION | Outbound webhooks | NO | NO | NO | subscription | Deliveries | | menus | CMS | ARCHITECTURAL FOUNDATION | Nav CMS | NO | YES | NO | — | Menus | | menu_items | CMS | ARCHITECTURAL FOUNDATION | Nav CMS | NO | NO | NO | menu | Items | | banners | CMS | ARCHITECTURAL FOUNDATION | Promo banners | NO | YES | NO | — | Banners | | tags | Catalog | DEFERRED | — | NO | YES | NO | — | Tags | | product_tags | Catalog | DEFERRED | — | NO | NO | NO | — | Pivot | | inventory_transfers | Inventory | DEFERRED | — | NO | YES | NO | — | Transfers | | inventory_transfer_items | Inventory | DEFERRED | — | NO | NO | NO | — | Transfer lines | | blog_posts | CMS | DEFERRED | — | NO | YES | YES | — | Blog | | blog_categories | CMS | DEFERRED | — | NO | YES | NO | — | Blog cats | | page_versions | CMS | DEFERRED | — | NO | NO | NO | page | Versioning | | theme_settings | CMS | DEFERRED | — | NO | NO | NO | — | Theme | | age_verification_providers | Compliance | DEFERRED | — | NO | YES | NO | — | Age verify registry | | admin_sessions | Identity | DEFERRED | — | NO | NO | NO | user | Extra session store if needed | | customer_addresses | Identity | ARCHITECTURAL FOUNDATION | Account UX | NO | NO | YES | customer_id | Saved addresses (guest-ready via carts) | \* Pre-arrival tables migrate when client confirms pre-arrival selling (feature gate). Contracts designed now. --- ## Exceptions (migrate foundation early) | Table | Exception? | Reason | |---|---|---| | `stores`, `inventory_locations` | Already REQUIRED | Multi-store foundation embedded in P1 FK design without speculative extra tables | | None other | — | No foundation tables require early migration to avoid destructive redesign | --- ## Counts (v1.2 Canonical) | Classification | Count | |---|---| | **REQUIRED NOW (migrate)** | **48** | | **ARCHITECTURAL FOUNDATION (no migrate)** | **36** | | **DEFERRED** | **10** | | **Total designed** | **94** | Note: v1.1 claimed 42/28/6 (~76) with speculative foundation migrations. v1.2 reduces **migrated** surface and adds missing designed tables (`incoming_inventory_reservations`, `redirects`, typed fulfillment FKs, etc.) without migrating them prematurely. ### Initial implementation migration set **Exactly the 48 REQUIRED NOW tables.** Laravel starter `users`/`cache`/`jobs` remain; domain migrations are additive. ================================================================================ FILE: docs/architecture/ARCHITECTURE_CONSISTENCY_MATRIX.md ================================================================================ # Architecture Consistency Matrix — v1.2 Defines transaction boundaries, locks, events, and failure recovery for critical workflows. **Rules:** 1. Module actions may join an **outer DB transaction** opened by an application orchestrator (same MySQL connection). 2. Modules still own invariants; orchestrator does not UPDATE foreign tables directly. 3. **External provider network calls NEVER run inside an open DB transaction.** 4. Typesense is never used for transactional decisions. --- ## Workflow Matrix | Workflow | API/Command | Orchestrator | Owning Module(s) | DB TX Boundary | Tables Mutated | Locks | State Transition | Ledger | Outbox Event | Async Consumers | External Call | Idempotency | Failure Recovery | |---|---|---|---|---|---|---|---|---|---|---|---|---|---| | Product create/update | Admin Catalog API | Catalog | Catalog | Single TX | products, variants, media links, outbox | — | — | — | product.* | Search, Audit | Storage put (before TX or after media row) | Admin write key optional | Retry safe | | Price change | Admin Pricing API | Pricing | Pricing | Single TX | prices, outbox | row on price | — | — | price.updated | Search, Audit | — | Optional | Replay | | Inventory receiving | Admin receive | Inventory | Inventory | Single TX | levels, transactions, outbox | FOR UPDATE level | — | RECEIVING +N | inventory.adjusted | Search, Audit | — | Required | Unique receive key | | Inventory adjustment | Admin adjust | Inventory | Inventory | Single TX | levels, transactions, outbox | FOR UPDATE level | Validate on_hand≥reserved after | ADJUSTMENT_* | inventory.adjusted | Search, Audit | — | Required | Reject if violates reserved | | Physical reservation | PlaceOrder (inner) | Checkout | Inventory | Outer checkout TX | reservations, levels.reserved | FOR UPDATE levels ordered | ACTIVE | none | inventory.reservation.created | Audit | — | Part of order key | Rollback order if fail | | Pre-arrival reservation | PlaceOrder (inner) | Checkout | Inventory | Outer checkout TX | incoming_reservations, incoming.reserved_qty | FOR UPDATE incoming | ACTIVE | none | incoming.reservation.created | Audit | — | Part of order key | Rollback if fail | | Checkout / order placement | POST /orders | **Checkout** | Orders + Inventory (+ reads) | **Outer TX: reserve + create order + outbox** | reservations, orders*, outbox | inventory locks | Order→PENDING_PAYMENT | none | order.placed | Notifications, Audit, Search (non-critical) | **None inside TX** | Required | No PENDING_PAYMENT without reservation | | Payment initiation | POST /payments | Payments | Payments | Short TX create payment+attempt | payments, attempts | — | Payment PENDING | — | — | — | **After commit:** PaymentGateway | Required | Unknown→reconcile | | Payment retry | POST /payments retry | Payments | Payments | Short TX new attempt | payment_attempts | payment row | Attempt FAILED stays; new attempt | — | payment.failed (attempt) | Orders stay PENDING_PAYMENT | Provider after commit | Required | Order not cancelled | | Payment webhook capture | Webhook | Payments | Payments → emits | TX: webhook dedup + payment CAPTURED + outbox | webhook_events, payments, transactions, outbox | payment FOR UPDATE | CAPTURED; Order CONFIRMED via sync call or event | none | payment.captured | Orders confirm, Fulfillment create | Verify signature only | provider_event_id unique | Duplicate ignored | | Reservation expiry | Scheduler | Inventory | Inventory | TX per reservation batch | reservations, levels.reserved | FOR UPDATE res+level | ACTIVE→EXPIRED | none | inventory.reservation.released | Orders may cancel if unpaid | — | Job idempotent | Skip if not ACTIVE | | Order cancel (unpaid) | Customer/Admin/System | Orders | Orders + Inventory | Outer TX | orders, reservations release | order + levels | CANCELLED | none | order.cancelled | — | — | Required | — | | Order cancel (paid) | Admin | Orders | Orders + Payments + Inventory/Fulfillment | Orchestrated steps | orders; refunds separate; releases | — | CANCELLED | reverse SALE if allocated | order.cancelled | Refund async | Refund provider **outside** TX | Required | Partial failure → reconcile | | Refund | Admin refund | Payments | Payments | Short TX + provider | refunds, payment status | payment FOR UPDATE | PARTIAL/FULL REFUNDED | — | payment.refunded | Orders display | Provider outside TX | Required | Provider idempotency | | Fulfillment create | On order.confirmed | Fulfillment | Fulfillment | TX | fulfillments, items, history, outbox | — | PENDING | none | fulfillment.created | — | — | By order id | — | | Inventory SALE commit | Allocate fulfillment | Fulfillment→Inventory | Inventory | TX | reservations COMMITTED, levels on_hand/reserved, transactions SALE, fulfillment ALLOCATED | FOR UPDATE level+reservation | Reservation COMMITTED; Fulfillment ALLOCATED | **SALE −N** | inventory.sale | Search | — | reservation id | Prevent double commit | | Pre-arrival receiving (partial) | Admin receive | Inventory | Inventory | TX | incoming, levels RECEIVING, convert reservations | FOR UPDATE incoming+levels | PARTIALLY_RECEIVED | RECEIVING | incoming.received | Convert to physical reservations | — | Required | Priority allocate paid orders | | Pickup ready/complete | Admin/Store | Fulfillment | Fulfillment | TX | fulfillment status | fulfillment | READY_FOR_PICKUP→PICKED_UP→COMPLETED | — | fulfillment.picked_up | Orders progress | — | Required | No shipment row | | Shipment create | After PACKED ship method | Fulfillment+Shipping | Fulfillment | Short TX then provider | shipments | — | PENDING | — | shipment.created | — | **Label create outside TX** | Required | Reconcile if timeout | | Tracking webhook | Carrier webhook | Shipping→Fulfillment | Fulfillment | TX dedup + status | tracking_events, shipment | shipment FOR UPDATE | Normalized status | — | shipment.* | Notifications | Verify signature | provider event id | Duplicate ignore | | Vendor sync | Scheduler | Vendors | Vendors | Per batch TX | vendor_offers, sync_runs | — | — | none | vendor.sync.* | Search availability flag | Connector **outside** long TX | Sync run id | Stale TTL | | Search reindex | Outbox consumer | Search | Search | Consumer TX optional | search_index_state | — | — | — | — | Typesense upsert | Typesense after claim | event_id | Retry; MySQL unaffected | --- ## Checkout Critical Invariant ``` BEGIN; Inventory.reserve(...) -- must succeed Orders.createPendingPayment(...) -- snapshots + PENDING_PAYMENT Outbox.write(order.placed) COMMIT; -- THEN payment provider call (outside TX) ``` If reserve fails → no order. If order insert fails → full rollback including reserve. `order.placed` async consumers **must not** enforce reservation or payment invariants. --- ## Inventory SALE Commit Point (Single Authority) | Moment | reserved | on_hand | Reservation status | Ledger | |---|---|---|---|---| | Checkout reserve | +qty | unchanged | ACTIVE | none | | Payment captured | unchanged | unchanged | ACTIVE (held) | none | | Fulfillment ALLOCATED | −qty | −qty | COMMITTED | SALE −qty | | Expiry/cancel before allocate | −qty | unchanged | EXPIRED/RELEASED | none | Double-commit prevention: `UPDATE reservations SET status='COMMITTED' WHERE id=? AND status='ACTIVE'` must affect 1 row. ================================================================================ FILE: docs/architecture/SEARCH_PROJECTION_MATRIX.md ================================================================================ # SEARCH_PROJECTION_MATRIX.md — Architecture v1.2 **Rule:** Typesense availability/pricing fields are **advisory**. Checkout must re-validate MySQL inventory, pricing, serviceability, and compliance. --- ## Indexed Document (products / variants) Primary collection: `products` (one document per **sellable** `product_variant`, denormalized with product/brand/category fields). | Indexed Field | Canonical Source | Module | Refresh Event(s) | Transformation | Staleness Tolerance | |---|---|---|---|---|---| | `id` (ULID) | product_variants.public_id | Catalog | product.created/updated | Direct | 0 (identity) | | `product_id` | products.public_id | Catalog | product.* | Direct | seconds | | `name` | products.name + variant label | Catalog | product.updated | Concat/display | seconds | | `slug` | products.slug / variant slug | Catalog | product.updated | Direct | seconds | | `sku` | product_variants.sku | Catalog | product.updated | Direct | seconds | | `brand` | brands.name | Catalog | product.updated, brand update | Join | seconds | | `brand_id` | brands.public_id | Catalog | product.updated | Direct | seconds | | `category_ids` | categories via pivot | Catalog | product.updated | Array of ULIDs | seconds | | `category_names` | categories.name | Catalog | product.updated | Array | seconds | | `liquor_type` | product_variants.liquor_type | Catalog | product.updated | Dedicated col | seconds | | `country` | product_variants.country_code | Catalog | product.updated | Dedicated col | seconds | | `region` | product_variants.region | Catalog | product.updated | Dedicated col | seconds | | `varietal` | product_variants.varietal | Catalog | product.updated | Dedicated col nullable | seconds | | `vintage` | product_variants.vintage | Catalog | product.updated | Dedicated col nullable | seconds | | `abv` | product_variants.abv | Catalog | product.updated | Decimal→float for facet | seconds | | `bottle_size_ml` | product_variants.bottle_size_ml | Catalog | product.updated | Int | seconds | | `pack_quantity` | product_variants.pack_quantity | Catalog | product.updated | Int | seconds | | `price_minor` | prices.amount_minor (active list) | Pricing | price.updated | Min/active store price | seconds–minutes | | `currency_code` | prices.currency_code | Pricing | price.updated | Direct | seconds | | `on_sale` / promo flag | promotions evaluation snapshot or flag | Promotions | promo change events | Boolean | minutes (advisory) | | `available` | inventory_levels computed OR vendor_offers | Inventory / Vendors | inventory.adjusted, inventory.sale, reservation.* (optional), vendor.sync.* | `any location available>0` OR vendor available & not stale | seconds–minutes (**advisory**) | | `availability_by_store` (optional later) | per-location available | Inventory | inventory.* | Nested map | minutes | | `is_active` | product/variant status & deleted_at | Catalog | product.deleted/updated | Boolean | seconds | | `created_at` | products.created_at | Catalog | product.created | Epoch | — | Configurable EAV attributes (when enabled) map to dynamic facet fields via attribute code → Typesense field registry. --- ## Event → Index Actions | Event | Action | |---|---| | product.created / updated | Upsert variant docs | | product.deleted | Soft-remove / is_active=false | | price.updated | Partial update price fields | | inventory.adjusted / inventory.sale / reservation release impacting availability | Partial update `available` | | vendor.sync.completed | Partial update vendor availability flags | | Full rebuild | Admin `search:rebuild` alias swap | --- ## Failure / Staleness - Index lag is acceptable for browsing. - Checkout ignores Typesense for stock/price truth. - `search_index_state` tracks last successful index per entity. - Typesense outage: storefront may degrade search UX; commerce path continues. ================================================================================ FILE: docs/architecture/CRITICAL_SCENARIO_TEST_MATRIX.md ================================================================================ # CRITICAL_SCENARIO_TEST_MATRIX.md — Architecture v1.2 | # | Scenario | Expected Invariant | Test Type | Lock / Idempotency Behavior | |---|---|---|---|---| | 1 | Two checkouts compete for last unit | Exactly one PENDING_PAYMENT order with ACTIVE reservation; other gets INVENTORY_UNAVAILABLE | Concurrency / feature | FOR UPDATE on inventory_levels; ordered locks | | 2 | Multi-item reservation deadlock prevention | Both succeed or one fails cleanly; no hang | Concurrency | Lock levels ascending (location_id, variant_id); deadlock retry ≤3 | | 3 | Reservation expiry vs payment webhook race | If CAPTURED before expiry commit, reservation stays ACTIVE; expiry skips non-ACTIVE | Concurrency | Conditional status update WHERE status=ACTIVE | | 4 | Duplicate payment webhook | Payment captured once; second webhook no-op | Webhook / integration | Unique provider_event_id | | 5 | Duplicate checkout request (same Idempotency-Key) | One order; second returns replayed response | Feature | Unique (actor, op, key); fingerprint match | | 6 | Same key different body | IDEMPOTENCY_CONFLICT | Feature | Fingerprint mismatch | | 7 | Payment retry after failed attempt | Order remains PENDING_PAYMENT; new attempt row; old FAILED immutable | Domain / feature | New attempt; no auto cancel | | 8 | Provider timeout after remote success | Status INDETERMINATE then reconcile to CAPTURED; no double charge | Integration | Provider idempotency key; reconcile job | | 9 | Idempotency unknown/indeterminate | No COMPLETED until known; safe retry policy | Domain | Status FAILED_SAFE_TO_RETRY vs INDETERMINATE | | 10 | Outbox concurrent workers | Each message processed once logically; consumers idempotent | Queue | FOR UPDATE SKIP LOCKED claim | | 11 | Outbox crash after Redis dispatch | At-least-once; consumer dedupe by event_id | Queue | PROCESSED only after successful handoff policy (see §13) | | 12 | Duplicate event delivery | Consumer side effects once | Unit/integration | event_id dedupe | | 13 | Redis outage | HTTP/DB path works; async delayed; no data corruption | Chaos | Outbox stays PENDING | | 14 | Typesense outage | Checkout still validates MySQL; search degraded | Chaos | Search jobs fail/retry | | 15 | Search stale availability | Checkout rejects if MySQL unavailable | Feature | Never trust Typesense for reserve | | 16 | Inventory adjustment against reservations | Cannot set on_hand < reserved; or discrepancy workflow | Domain | FOR UPDATE; CHECK + domain guard | | 17 | Partial fulfillment cancellation | Remaining qty cancelled; order completion by qty semantics | Domain | Order not COMPLETED solely because fulfillments terminal | | 18 | Refund concurrency | Sum(refunds) ≤ captured; no over-refund | Concurrency | payment FOR UPDATE | | 19 | Pre-arrival oversell | available_prearrival never negative | Concurrency | FOR UPDATE incoming_inventory | | 20 | Partial pre-arrival receiving | 50 of 100 received; allocate paid commitments first; remainder stay pre-arrival | Domain | Receiving TX + conversion rules | | 21 | Vendor stale availability | Stale offers not sellable as guaranteed stock | Feature | sync_status/TTL | | 22 | Vendor qty changed after sync | Snapshot informational unless capability supports reserve | Domain | Capability gates | | 23 | Shipment webhook duplication | One state transition | Webhook | Dedup key | | 24 | Unknown carrier status | Raw stored; internal state unchanged | Adapter | Normalize map | | 25 | Pickup completion | No shipment; READY_FOR_PICKUP→PICKED_UP→COMPLETED | Domain | Method=PICKUP | | 26 | Order cancellation after capture | Refund initiated; reservations/allocations released correctly | Domain | Orchestrated cancel | | 27 | Double SALE commit attempt | Second ALLOCATED commit no-ops / conflicts | Domain | Conditional COMMITTED update | | 28 | Payment FAILED does not cancel order | Order PENDING_PAYMENT until expiry | Domain | Explicit | | 29 | External call not inside DB TX | No open transaction during Stripe/UPS HTTP | Integration test / code review | Assert TX closed | | 30 | Guest cart → registered later (if enabled) | Cart merge without losing lines | Feature | customer_id nullable | Each scenario maps to PHPUnit feature or domain tests under `tests/` when implementation begins. ================================================================================ FILE: docs/architecture/01-system-overview.md ================================================================================ ## 01. System Overview — v1.2 ### Goal API-first modular monolith for liquor commerce on **Laravel 13**, MySQL 8, Redis queues, Typesense projection, S3-compatible storage, Cloudflare edge. ### Non-goals (Phase 1) No microservices, Kafka, Elasticsearch, GraphQL, MongoDB, AI/MCP implementation, SGProof scraping implementation. ### Checkout Transaction Strategy (v1.2) Checkout is an **application orchestrator**, not a table owner. **Synchronously consistent (same DB transaction):** 1. Inventory physical and/or pre-arrival reservation 2. Order create in `PENDING_PAYMENT` with snapshots 3. Outbox `order.placed` **Invariant:** Order must not become `PENDING_PAYMENT` unless required reservations succeed. **Eventually consistent (after commit):** - Search indexing - Notifications - Webhook fan-out - Fulfillment creation (after `payment.captured` / `order.confirmed`) **Outside DB transactions:** - PaymentGateway / ShippingProvider / VendorConnector / Typesense / Storage network calls Module actions participate in an outer transaction via the shared DB connection; each module still enforces its own invariants. ### Inventory Commit SALE ledger + `on_hand` decrease occurs only at **fulfillment ALLOCATED**, not at payment capture. ### Search Typesense is advisory. Checkout re-validates MySQL. ### Framework Architecture baseline: **Laravel 13** (matches `composer.json`). ================================================================================ FILE: docs/architecture/02-domain-boundaries.md ================================================================================ ## 02. Domain Boundaries — v1.2 See `MODULE_OWNERSHIP_MATRIX.md` for canonical table ownership. ### Design Rules 1. Single MySQL database (Phase 1). 2. One owner per table; no cross-module direct mutation. 3. Interaction via contracts, orchestrators, and outbox events. 4. Checkout owns **no** commerce tables. 5. Store owns `inventory_locations`; Inventory owns quantities. 6. Fulfillment owns shipment aggregates; Shipping owns provider config/orchestration. 7. Search projection is not source of truth. ### Modules Identity, Catalog, Media, Store/Location, Inventory, Pricing, Promotions, Cart, Checkout (orchestrator), Orders, Payments, Fulfillment, Shipping, Vendors, Compliance, CMS, SEO, Search, Integration, Notification, Audit, Platform (outbox/idempotency). ### Sellable Unit Rule CartItem, Price, Inventory, Vendor mapping, OrderItem → **ProductVariant**. Product has no inventory qty or canonical retail price. ================================================================================ FILE: docs/architecture/03-database-architecture.md ================================================================================ ## 03. Database Architecture — v1.2 Canonical: MySQL 8. Projection: Typesense. Framework: Laravel 13. ### Identifiers (ADR-002) BIGINT UNSIGNED PK internal; ULID `public_id` on API-facing resources only (see TABLE_REGISTRY). ### Money (ADR-004) Integer minor units + ISO 4217 `currency_code`. ### Quantities INT bottles/units; no fractional Phase 1. ### Constraints See `27-database-constraints.md`. Classify invariants as DB / transactional domain / async reconcile. ### Soft delete Catalog/CMS may soft-delete. Never soft-delete orders, payments, refunds, ledger, audit, outbox. ### Historical FK `ON DELETE RESTRICT` for financial/order/ledger FKs. Discontinued variants remain for historical order_items snapshots. ### Table counts **Source of truth:** `TABLE_REGISTRY.md` — 48 migrate now / 36 foundation schema-less / 10 deferred. ================================================================================ FILE: docs/architecture/04-catalog.md ================================================================================ ## 04. Catalog Architecture — v1.2 (Attribute Freeze) ### Sellable Unit - **Product** = merchandising/logical entity - **ProductVariant** = sellable unit (SKU) ### Attribute Strategy (Frozen Hybrid) | Characteristic | Storage | Why | |---|---|---| | liquor_type / alcohol category | Dedicated column on variant (or product) | Core filter | | brand | Relationship `brands` | Taxonomy + SEO | | country_code | Dedicated column | Filter | | region | Dedicated column | Filter | | varietal | Dedicated nullable column | Wine filter | | vintage | Dedicated nullable column/year | Filter | | abv | Dedicated DECIMAL(5,2) | Filter/sort | | bottle_size_ml | Dedicated INT | Filter | | pack_quantity | Dedicated INT | Pack sells | | sku | Dedicated unique nullable | Ops | | upc_ean | Dedicated nullable | Ops | | producer | Configurable attribute or dedicated if always needed | Prefer attribute if sparse | | appellation | Configurable attribute | Sparse | | rating | Configurable / external | Sparse | | tasting notes | Metadata/CMS | Not filter-critical | Do not make everything nullable product columns. Do not make every filter EAV/JSON. Dedicated typed columns required for Phase-1 filters. EAV tables (`product_attributes*`) are foundation for extension — migrate when needed. ### Slugs Products, categories, brands, collections, CMS pages support SEO slugs. Unique per entity type. Slug changes create `redirects`. Canonical storefront routes use slugs; APIs use ULID; DB uses BIGINT. ### Collections Homepage featured collections may start as CMS section config referencing product/variant IDs; full `collections` tables when merchandising needs them. ================================================================================ FILE: docs/architecture/05-inventory.md ================================================================================ ## 05. Inventory Architecture — v1.2 See ADR-003 (updated) and `ARCHITECTURE_CONSISTENCY_MATRIX.md`. ### Ownership - **Store** owns `inventory_locations` (identity). - **Inventory** owns `inventory_levels`, `inventory_transactions`, `inventory_reservations`, `incoming_inventory`, `incoming_inventory_reservations`. ### Quantity Semantics | Field | Storage | Definition | |---|---|---| | `on_hand` | Persisted | Physical stock | | `reserved` | Persisted | Sum of ACTIVE physical reservations | | `available` | **Computed** | `on_hand - reserved` | Constraints (DB + transactional): - `on_hand >= 0`, `reserved >= 0` - `reserved <= on_hand` - Stock-decreasing ops **must not** produce `on_hand < reserved` ### Ledger (physical only) Reservations are **not** ledger rows. Types: `RECEIVING`, `SALE`, `RETURN`, `ADJUSTMENT_IN`, `ADJUSTMENT_OUT`, `TRANSFER_IN/OUT` (deferred), `DAMAGE`, `SYNC_CORRECTION`. ### Reservation States (physical) | State | Meaning | |---|---| | `ACTIVE` | Hold after checkout (includes post-payment until allocated) | | `COMMITTED` | Consumed into SALE at fulfillment allocation | | `RELEASED` | Cancelled before commit | | `EXPIRED` | TTL elapsed while unpaid/abandoned rules apply | | `CANCELLED` | Admin/system cancel | TTL is **configurable** (not hard-coded). ### Authoritative SALE Commit Point (v1.2 — resolves contradiction) **Only fulfillment allocation commits stock:** | Event | reserved | on_hand | Reservation | Ledger | |---|---|---|---|---| | Checkout reserve | +N | — | ACTIVE | none | | Payment captured | — | — | ACTIVE (held) | none | | Fulfillment → ALLOCATED | −N | −N | COMMITTED | SALE −N | | Expire/cancel before allocate | −N | — | EXPIRED/RELEASED | none | Double-commit guard: ```sql UPDATE inventory_reservations SET status = 'COMMITTED', committed_at = NOW() WHERE id = ? AND status = 'ACTIVE'; -- must affect exactly 1 row ``` ### Concurrency Algorithm ``` BEGIN; SELECT ... FROM inventory_levels WHERE ... ORDER BY location_id, variant_id FOR UPDATE; IF on_hand - reserved < qty THEN fail INVENTORY_UNAVAILABLE; INSERT reservation ACTIVE; UPDATE levels SET reserved = reserved + qty; COMMIT; ``` Deadlock: retry ≤3. Redis not used for correctness. ### Adjustments vs Reservations Admin adjustment locks level and validates resulting `on_hand >= reserved`. If physical count discovers `on_hand < reserved`: 1. Do **not** silently clobber. 2. Enter **discrepancy workflow**: flag level, alert ops, block further outbound allocations until resolved (release excess reservations / adjust with audit / cancel orders per policy). 3. Reconciliation jobs detect drift; they do not auto-fix. ### Pre-arrival See `24-pre-arrival-vendor-inventory.md`. Never inflate `on_hand` until RECEIVING. ================================================================================ FILE: docs/architecture/06-pricing-promotions.md ================================================================================ ## 06. Pricing & Promotions — v1.2 Pricing owns retail `prices.amount_minor`. Vendor cost lives on `vendor_offers.vendor_price_minor` and is never used as retail at checkout. Promotions/coupons are architectural foundation — migrate when launch requires them. Free-shipping eligibility is a promotion action + shipping quote interaction when enabled. Tax via TaxProvider; order snapshots store tax amounts. ================================================================================ FILE: docs/architecture/07-orders-fulfillment.md ================================================================================ ## 07. Orders & Fulfillment — v1.2 See `22-state-machines.md`, ADR-008, CONSISTENCY_MATRIX. ### Ownership - Orders owns order aggregates + snapshots + status history. - Fulfillment owns fulfillments + shipments + tracking. - Shipping owns methods/rates/providers + adapter orchestration (does not own shipment rows). ### Fulfillment Source Referential Integrity Prefer nullable typed FKs: - `inventory_location_id` XOR `vendor_id` (check constraint / domain guard) Avoid polymorphic `source_type + source_id` without FK. ### Pickup Method `PICKUP` on fulfillment. States `READY_FOR_PICKUP` / `PICKED_UP`. No fake shipment. ### Partial / Multi-source Architecturally supported. Phase-1 ops may use single source; schema ready. ### Order Completion Quantity/outcome based — see §22.1. ================================================================================ FILE: docs/architecture/08-payments.md ================================================================================ ## 08. Payments Architecture — v1.2 ### Separation Order ≠ Payment. Payments module owns financial history. ### Money `amount_minor` + `currency_code` (ADR-004). ### Retry Semantics (v1.2) Failed **attempt** ≠ failed **order**. ``` Order: PENDING_PAYMENT Attempt1 FAILED Attempt2 FAILED Attempt3 CAPTURED → payment.captured → Order CONFIRMED ``` Cancel unpaid order only on expiry / customer/admin cancel / abandon policy — not on attempt failure. ### Aggregates | Table | Mutability | |---|---| | payments | Aggregate status updates | | payment_attempts | Immutable outcomes | | payment_transactions | Append-only | | refunds | Append-only | | payment_webhook_events | Insert + process once (`provider_event_id` unique) | ### Provider calls Never inside open MySQL TX. Use provider idempotency keys. Timeouts → `INDETERMINATE` until reconcile. ### Webhooks Signature verify → dedupe → transition → outbox. Idempotent. ### After capture Inventory SALE is **not** committed here. Reservation remains ACTIVE until fulfillment ALLOCATED (see §05). ================================================================================ FILE: docs/architecture/09-shipping.md ================================================================================ ## 09. Shipping — v1.2 ### Ownership Shipping owns provider configuration and orchestration contracts. Fulfillment owns shipment rows/tracking. ### Capability Contracts Adapters may support subsets of: rates, createShipment, labels, track, cancel, sameDay/localDelivery, pickup coordination. Core Orders/Fulfillment never import provider SDKs. Provider metadata stays adapter/integration-owned. ### Future partners Add adapter + config rows; no core schema rewrite. ================================================================================ FILE: docs/architecture/10-vendors.md ================================================================================ ## 10. Vendors — v1.2 SGProof is not core domain. Capability-based VendorConnector (see INTEGRATION_CONTRACTS + §24). Schema foundation until vendor phase. Never merge vendor qty into `inventory_levels`. ================================================================================ FILE: docs/architecture/11-compliance.md ================================================================================ ## 11. Compliance — v1.2 ### Decision Contract ``` ComplianceDecision { allowed: bool denial_codes: string[] required_actions: string[] // e.g., VERIFY_AGE rule_refs: [{ rule_id, version, effective_from, effective_to? }] evaluated_at: datetime context_snapshot: { destination, method, store, product refs... } } ``` Rules support `version`, `effective_from`, `effective_to`. Order snapshots store decision context for audit. Do not hard-code liquor laws without client/legal approval. ### Checkpoints | Checkpoint | Purpose | |---|---| | Catalog visibility | Optional hide restricted SKUs | | Checkout / order placement | Must pass before PENDING_PAYMENT | | Fulfillment | Re-check if destination/method changes | | Shipment / delivery | Handoff rules | | Pickup handoff | Age verification at pickup may still be required | Checkout verification does **not** automatically satisfy delivery/pickup handoff requirements. ================================================================================ FILE: docs/architecture/12-api-standards.md ================================================================================ ## 12. API Standards — v1.2 See `API_CONVENTIONS.md` and ADR-009. Versioning `/api/v1/`. Resources/DTOs only. OpenAPI first-class. Idempotency on critical writes. Cursor pagination without mandatory totals. ================================================================================ FILE: docs/architecture/13-events-outbox.md ================================================================================ ## 13. Events & Transactional Outbox — v1.2 See ADR-005 (updated). ### Write Pattern ``` BEGIN; domain mutation; INSERT outbox PENDING; COMMIT; ``` Then publisher claims and dispatches to Redis queue. **No DB locks during external side effects.** ### Claim (MySQL 8) ``` BEGIN; SELECT id FROM outbox_messages WHERE status='PENDING' AND available_at <= NOW() ORDER BY id ASC LIMIT N FOR UPDATE SKIP LOCKED; UPDATE ... SET status='PROCESSING', locked_at=NOW(), attempts=attempts+1; COMMIT; -- perform side effects / dispatch -- then mark PROCESSED or schedule retry ``` ### When is PROCESSED? **Definition (v1.2):** Mark `PROCESSED` after the outbox publisher has successfully **enqueued** the Laravel job to Redis (or executed in-process consumer start handoff), not after all downstream consumers finish. Rationale: consumers are independently idempotent; outbox responsibility is durable handoff to the queue. | Failure | Behavior | |---|---| | DB committed, Redis down | Remain PROCESSING/PENDING with backoff; retry enqueue | | Redis accepted, worker crash | Job retry; consumer idempotent by event_id | | Consumer success, ack fail | At-least-once; dedupe | | Duplicate delivery | Consumer no-op | ### Retry Max 10; exponential backoff; stuck PROCESSING > 5m → PENDING; manual replay for FAILED. ### Event Identifier Strategy | Context | ID type | |---|---| | outbox `event_id` | ULID | | Internal cross-module payload refs | Prefer `*_public_id` for aggregates that have public IDs; internal BIGINT allowed **only** for non-API internal consumers and never in external webhooks | | External webhooks | Public ULIDs only; never sequential PKs | | Provider webhook ids | Provider event id string | Do not add `public_id` to every table (e.g., inventory_levels, attempts). ### order.placed Responsibility Async only: notifications, audit, search hints. Does **not** create fulfillment or capture payment. Fulfillment starts after `order.confirmed` / payment captured. ================================================================================ FILE: docs/architecture/14-webhooks-integrations.md ================================================================================ ## 14. Webhooks & Integrations — v1.2 Outbound webhooks and integration registry are foundation (schema when needed). External webhook payloads use public ULIDs only. Signature verify + delivery retries + unhealthy endpoint disable remain as designed in v1.1 conceptually. ================================================================================ FILE: docs/architecture/15-auth-security.md ================================================================================ ## 15. Auth & Security — v1.2 ### Identity Model | Principal | Table | Purpose | |---|---|---| | Admin/staff | `users` + RBAC | Super Admin | | Storefront | `customers` | Customer accounts | | Integration | API credentials / OAuth | Machine clients | Separate auth stacks intentionally; shared password hashing primitives OK. ### Guest Checkout If required, carts support `customer_id` NULL + guest token; conversion attaches customer later. Confirm in open decisions. ================================================================================ FILE: docs/architecture/16-search.md ================================================================================ ## 16. Search — v1.2 See ADR-007 and `SEARCH_PROJECTION_MATRIX.md`. MySQL canonical. Typesense projection. Alias-swap rebuild. **Checkout must never trust Typesense for stock, price, serviceability, or compliance.** Field/facet definitions follow catalog attribute freeze (§04) + SEARCH_PROJECTION_MATRIX. ================================================================================ FILE: docs/architecture/17-cms-seo.md ================================================================================ ## 17. CMS & SEO — v1.2 CMS independent of commerce. Homepage sections REQUIRED. SEO metadata + redirects REQUIRED for slug changes. Address model: - `store_addresses` — store ops - `order_addresses` — immutable snapshots - `customer_addresses` — foundation for account UX Slugs for storefront; ULID for APIs; BIGINT for DB. ================================================================================ FILE: docs/architecture/18-observability.md ================================================================================ ## 18. Observability & Audit — v1.2 ### Audit Append-only **while retained**. Retention/archival/purge is a separate lifecycle under approved privacy policy — not “never delete forever” and “configurable retention” without explanation. Lifecycle: 1. Hot retain (queryable) 2. Archive (cold storage) after policy threshold 3. Purge only under legal/privacy approval ### Polymorphic actor/resource refs Intentionally **no hard FKs**. Historical evidence may outlive resources. Store public_ids + snapshots sufficient to interpret after deletion/deactivation. ### PII / Sensitive Classification Do **not** dump `model->toArray()`. | Class | Examples | Audit policy | |---|---|---| | Secrets | passwords, tokens, API keys, webhook secrets, card data | Never store; `[REDACTED]` | | PII sensitive | DOB, ID docs, verification payloads | Redact or hash; store decision outcome only | | PII operational | email, phone, address | Allowlist per action; prefer truncated/masked unless required | | Business | prices, qty, status | Allowed | Module-specific audit payloads (allowlists) required. ================================================================================ FILE: docs/architecture/19-testing.md ================================================================================ ## 19. Testing — v1.2 See `CRITICAL_SCENARIO_TEST_MATRIX.md` for required concurrency/failure scenarios. Layers: unit, domain, feature/API, contract (OpenAPI), adapter mocks, queue/outbox, search projection rebuild, inventory concurrency, payment idempotency, webhooks. ================================================================================ FILE: docs/architecture/20-deployment.md ================================================================================ ## 20. Deployment — v1.2 (Laravel 13) ### Processes | Process | Responsibility | |---|---| | Laravel HTTP/API | Sync requests | | Queue workers | Domain consumers, webhooks outbound, search jobs | | Outbox publisher worker | Claim outbox → Redis enqueue | | Scheduler | Expiry, stuck recovery, vendor sync triggers, reconcile | | MySQL | Canonical data | | Redis | Cache + queues | | Typesense | Search projection | ### Queue names (suggested) `outbox`, `default`, `search`, `webhooks`, `notifications`, `integrations` ### Ops Graceful worker restart on deploy; failed_jobs table; health/readiness: DB, Redis, queue lag, outbox lag, Typesense optional. Container-ready; Kubernetes not required. ### Migrations Expand-only compatible; avoid destructive changes without dual-write windows. ================================================================================ FILE: docs/architecture/21-open-decisions.md ================================================================================ ## 21. Open Decisions — v1.2 ### Resolved in v1.2 (technical) | Decision | Resolution | |---|---| | inventory_locations owner | Store | | SALE commit point | Fulfillment ALLOCATED only | | Payment failed attempt | Does not auto-cancel order | | Order completion | Quantity/outcome based | | Foundation migrations | Do not migrate speculative tables | | Pre-arrival reservations | `incoming_inventory_reservations` (Option A) | | Fulfillment source RI | Typed nullable FKs | | Pickup | Fulfillment method + READY_FOR_PICKUP/PICKED_UP | | Outbox PROCESSED meaning | After successful Redis enqueue | | Event IDs | ULID event_id; webhooks public IDs only | | Laravel version | 13 | | Identity | users=admin/staff; customers=storefront | | Checkout TX | Outer TX for reserve+order+outbox; providers outside | | Vendor cost vs retail | Explicit separation | | Search transactional use | Forbidden | | Audit retention | Hot → archive → purge under policy | | Pagination totals | Optional for cursor APIs | ### Remaining Business Decisions #### Blocks schema of affected feature (not whole platform) | # | Decision | Blocks | |---|---|---| | B1 | Pre-arrival selling at launch? | incoming_* migrations & checkout paths | | B2 | Pickup at launch? | Pickup UX + compliance handoff rules (schema mostly ready via fulfillment.method) | | B3 | Guest checkout required? | carts.customer_id null already designed; confirm UX/auth | | B4 | Initial compliance rule set / jurisdictions | compliance_* migrations & checkout gates content | | B5 | Age verification at checkout and/or pickup? | Provider adapter + checkpoint config | | B6 | Promotions/coupons at launch? | promotions* migrations | | B7 | Collections as first-class vs CMS-only? | collections tables | #### Configurable / confirm but do not block core commerce schema | # | Decision | |---|---| | C1 | USD confirmation | | C2 | Reservation TTL default (15m proposed) | | C3 | Async payment extension window | | C4 | Split fulfillment at launch (schema ready) | | C5 | Shipping zone granularity | | C6 | Audit retention defaults | | C7 | Customer auth session vs token for Next.js | | C8 | Admin 2FA at launch | | C9 | Tax provider selection | | C10 | Notification channels for P1 | ### Implementation readiness note Core P1 path (catalog, inventory, cart, checkout, orders, payments, ship fulfillment, CMS/SEO, search, audit, outbox) is **technically unblocked**. Feature-gated items above block only those features. ================================================================================ FILE: docs/architecture/22-state-machines.md ================================================================================ ## 22. State Machines — v1.2 Statuses stored as `VARCHAR`; application enums. History tables capture: `from_status`, `to_status`, `actor_type` (CUSTOMER|ADMIN|SYSTEM|PROVIDER), actor ref, `reason_code`, details, `correlation_id`, `created_at`. --- ### 22.1 Order | State | Description | |---|---| | `DRAFT` | Pre-submit (optional; may be client-only) | | `PENDING_PAYMENT` | Placed; inventory reserved; awaiting successful payment | | `CONFIRMED` | Payment captured; eligible for fulfillment | | `PROCESSING` | Fulfillment in progress | | `PARTIALLY_FULFILLED` | Some qty fulfilled; remainder open | | `COMPLETED` | All **sellable qty** fulfilled successfully (not merely terminal fulfillments) | | `CANCELLED` | Fully cancelled (terminal) | | `CLOSED` | Mixed outcome closed (e.g., partial fulfill + cancelled remainder) — optional display alias of completed business close | #### Payment failure rule (v1.2) `payment_attempt FAILED` does **NOT** auto-transition order to CANCELLED. Cancel `PENDING_PAYMENT` only when: - reservation/payment window expires - customer cancels - admin cancels - abandoned payment policy fires #### Completion semantics (v1.2) Do **not** use “all fulfillments terminal ⇒ COMPLETED”. Compute from order item quantities: - `qty_ordered` - `qty_fulfilled` - `qty_cancelled` - `qty_returned` (post) `COMPLETED` when `qty_fulfilled + qty_cancelled == qty_ordered` AND `qty_fulfilled > 0` AND no open fulfillments. `CANCELLED` when `qty_cancelled == qty_ordered`. `CLOSED`/partial policies when partial fulfill + cancelled remainder (document in ops policy). #### Key transitions | From | To | Trigger | Guards | Side effects | Event | |---|---|---|---|---|---| | — | PENDING_PAYMENT | Checkout | Reserve OK; compliance OK | Create order+snapshots | order.placed | | PENDING_PAYMENT | CONFIRMED | payment.captured | Payment CAPTURED | — | order.confirmed | | PENDING_PAYMENT | CANCELLED | expiry/cancel | Unpaid window | Release reservations | order.cancelled | | CONFIRMED | PROCESSING | fulfillment started | — | — | — | | * | COMPLETED/CANCELLED/CLOSED | qty rules | — | — | order.completed / cancelled | --- ### 22.2 Payment Aggregate vs Attempt vs Transaction | Concept | Role | |---|---| | `payments` | Aggregate status for the order payment | | `payment_attempts` | Immutable tries; FAILED attempt never becomes success | | `payment_transactions` | Append-only provider money movements | #### Aggregate states `PENDING`, `AUTHORIZED`, `CAPTURED`, `VOIDED`, `PARTIALLY_REFUNDED`, `REFUNDED` Attempt may be `FAILED` while aggregate remains `PENDING` (retryable). --- ### 22.3 Fulfillment (+ Pickup) | State | Description | |---|---| | `PENDING` | Created after order confirmed | | `ALLOCATED` | **SALE commit** via Inventory (authoritative) | | `PICKING` | Picking | | `PACKED` | Packed | | `READY_FOR_PICKUP` | Pickup method only | | `PICKED_UP` | Customer collected | | `SHIPPED_CLOSED` | Linked shipment delivered (ship method) — or use COMPLETED | | `COMPLETED` | Successfully handed off (shipped delivered OR picked up) | | `CANCELLED` | Cancelled | **Pickup:** use fulfillment method `PICKUP`. **Do not** create a fake `shipments` row. Ship method: create shipment after PACKED; tracking drives delivery → fulfillment COMPLETED. Allocation side effect: Inventory reservation ACTIVE→COMMITTED + SALE ledger. Typed FKs on fulfillments: `inventory_location_id` nullable, `vendor_id` nullable (exactly one set for Phase 1 sources). Prefer over polymorphic `source_type/source_id` for RI. --- ### 22.4 Shipment | State | Description | |---|---| | `PENDING` | Record created | | `READY` | Label created | | `SHIPPED` | With carrier | | `IN_TRANSIT` | Moving | | `EXCEPTION` | Carrier exception (**not** terminal) | | `DELIVERED` | Terminal success | | `FAILED` | Terminal failure after exception exhausted / declared | | `CANCELLED` | Cancelled pre-pickup | | `RETURNED` | Returned | Provider raw status stored separately; normalize into internal states. Unknown provider status → log only. --- ### 22.5 Reservation (physical) — see §05 ACTIVE → COMMITTED (at ALLOCATED) | RELEASED | EXPIRED | CANCELLED ================================================================================ FILE: docs/architecture/23-idempotency.md ================================================================================ ## 23. Idempotency — v1.2 ### Key Composition `UNIQUE (actor_scope, operation_scope, idempotency_key)` + `request_fingerprint` SHA-256. ### States | Status | Meaning | |---|---| | `PROCESSING` | In flight (short DB claim) | | `COMPLETED` | Safe replay of stored response | | `FAILED_SAFE_TO_RETRY` | Known failure before side effects; retry allowed | | `INDETERMINATE` | Provider timeout / unknown remote outcome; reconcile before retry charge | **Never hold MySQL transactions open during external network calls.** Flow: 1. Short TX: insert/claim idempotency PROCESSING 2. Commit 3. External call (if any) with provider idempotency key 4. Short TX: persist domain result + COMPLETED response (or INDETERMINATE) ### Retention Configurable by operation class (orders/payments longer than cart writes). ### Critical ops Order create, payment initiate/capture, refund, inventory adjust, shipment create, integration writes. ================================================================================ FILE: docs/architecture/24-pre-arrival-vendor-inventory.md ================================================================================ ## 24. Pre-Arrival & Vendor Inventory — v1.2 ### 24.1 Pre-Arrival Model Pre-arrival is **not** a product type. Physical `on_hand` never includes expected incoming stock. #### Tables **`incoming_inventory`** (aggregate expected shipment of a variant to a location) **`incoming_inventory_reservations`** (Option A — preferred for clarity) | Column (reservations) | Notes | |---|---| | id | BIGINT PK | | incoming_inventory_id | FK | | order_id | FK | | order_item_id | FK nullable | | quantity | > 0 | | status | ACTIVE, CONVERTED, RELEASED, EXPIRED, CANCELLED | | expires_at | nullable / policy-driven | | converted_reservation_id | FK to physical inventory_reservations when converted | | created_at | | #### Availability ``` available_prearrival = expected_quantity - reserved_quantity ``` `reserved_quantity` is denormalized aggregate of ACTIVE (and optionally CONVERTED-pending) reservation qty; updated atomically with row lock. #### Concurrency ``` BEGIN; SELECT * FROM incoming_inventory WHERE id=? FOR UPDATE; IF expected - reserved < qty THEN fail; INSERT incoming_inventory_reservations ACTIVE; UPDATE incoming_inventory SET reserved_quantity = reserved_quantity + qty; COMMIT; ``` #### Partial receiving (example: expected 100, pre-sold 80, received 50) 1. Lock incoming + destination inventory_level. 2. RECEIVING +50 to `on_hand`. 3. Allocate received units to ACTIVE pre-arrival reservations by **priority** (configurable: earliest paid order first). 4. For each allocated reservation qty Q: - Create physical `inventory_reservations` ACTIVE (or HELD) for Q against level. - Mark incoming reservation CONVERTED (or partially convert via split rows / remaining qty field). - Decrement incoming `reserved_quantity` for converted portion; decrement remaining expected. 5. Status → `PARTIALLY_RECEIVED` if expected remaining > 0. 6. Unfulfilled pre-sold qty remain on incoming until future receipts, cancellation, or refund path. 7. Notify customers of delays; audit each allocation; refunds via Payments if cancelled. Do not assume 100% arrival. #### Feature gate Tables are **ARCHITECTURAL FOUNDATION** — migrate when client confirms pre-arrival selling (`21-open-decisions`). --- ### 24.2 Vendor Inventory & Capabilities Vendor snapshot quantity ≠ our ledger guarantee. #### Capability-oriented VendorConnector (illustrative) ``` interface VendorConnector { healthCheck(): Health; capabilities(): VendorCapabilities; } interface VendorCapabilities { supportsCatalogFetch: bool; supportsAvailabilitySync: bool; // informational supportsReservation: bool; // hold at vendor supportsOrderPlacement: bool; // confirmable purchase supportsCancellation: bool; supportsTracking: bool; supportsRealtimePricing: bool; } ``` | Capability level | Oversell guarantee | |---|---| | Informational availability only | None — display/sync only; checkout must not treat as reserved stock | | Reservable | Hold via vendor API then local tracking | | Order-confirmable | Confirm vendor order before promising customer | #### Prices - `vendor_offers.vendor_price_minor` = **procurement/source cost** - `prices.amount_minor` = **customer retail** - Checkout never uses vendor cost as retail. Vendor qty never merges into `inventory_levels.on_hand`. ================================================================================ FILE: docs/architecture/25-multi-store-serviceability.md ================================================================================ ## 25. Multi-Store & Serviceability — v1.2 ### Entities Store ≠ InventoryLocation ≠ Address ≠ ServiceZone. ### SERVICEABILITY vs RANKING - **SERVICEABILITY** = which sources are eligible (destination, compliance, method, stock, vendor capability). - **RANKING** = preference among eligible sources (business priority, distance, completeness, SLA, cost, vendor fallback). Nearest store is **never** automatically canonical fulfillment logic. Distance is a ranking input for **nearby store visibility**, not sole selector. ### Eligibility Targets (no generic fulfillment_sources table unless later justified) | Source | Eligibility | |---|---| | Store / inventory location | Zone + stock + method (ship/pickup) | | Vendor | Capability + offer freshness + method | | Shipping provider/method | Zone + rates + compliance | ### Pickup Store pickup eligibility is a serviceability outcome; fulfillment method `PICKUP`. ### Phase 1 Single store operationally OK; schema includes stores + locations for expansion without rewrite. ================================================================================ FILE: docs/architecture/26-phase1-table-classification.md ================================================================================ ## 26. Phase-1 Table Classification — v1.2 **Superseded by `TABLE_REGISTRY.md`.** This file summarizes only. | Classification | Count | Migrate? | |---|---|---| | REQUIRED NOW | 48 | YES | | ARCHITECTURAL FOUNDATION | 36 | NO (design/contracts only) | | DEFERRED | 10 | NO | v1.1 incorrectly planned migrating ~28 foundation tables. v1.2 does **not**. Exceptions: none beyond stores/locations already REQUIRED for inventory FKs. ================================================================================ FILE: docs/architecture/27-database-constraints.md ================================================================================ ## 27. Database Constraints — v1.2 ### Invariant Classes | Class | Examples | |---|---| | DB enforced | PK/FK/UNIQUE/CHECK non-negative, reserved<=on_hand, qty>0, amount>0 | | Transactional domain | available enough to reserve; payment capture guards; double-commit prevention | | Async reconcile | levels vs ledger sum; search lag | ### Critical Checks - inventory_levels: on_hand>=0, reserved>=0, reserved<=on_hand; UNIQUE(location,variant) - reservations: quantity>0; UNIQUE(idempotency_scope,key) - prices/payments/refunds: amount_minor>=0 or >0 as applicable; currency NOT NULL - order_items: quantity>0 - fulfillments: (location_id IS NOT NULL) XOR (vendor_id IS NOT NULL) for Phase-1 sources - idempotency_keys: UNIQUE(actor, op, key) - outbox: UNIQUE(event_id) - payment_webhook_events: UNIQUE(provider_event_id) Do not use CHECK for cross-row aggregates MySQL cannot enforce (e.g., sum of reservations equals reserved) — maintain transactionally + reconcile. ================================================================================ FILE: docs/architecture/API_CONVENTIONS.md ================================================================================ ## API_CONVENTIONS.md — v1.2 Framework: Laravel 13 APIs under `/api/v1/`. ### Identifiers - API: ULID `public_id` - Storefront SEO routes: `slug` where applicable - DB: BIGINT (never in public API) ### Success Envelope `{ data, meta?, request_id, correlation_id }` ### Error Envelope ```json { "error": { "code": "INVENTORY_UNAVAILABLE", "message": "...", "details": {} }, "request_id": "..." } ``` Codes: VALIDATION_ERROR, AUTHENTICATION_REQUIRED, FORBIDDEN, RESOURCE_NOT_FOUND, CONFLICT, INVENTORY_UNAVAILABLE, INVALID_STATE_TRANSITION, IDEMPOTENCY_CONFLICT, RATE_LIMITED, INTEGRATION_FAILURE, INTERNAL_ERROR. ### Pagination Cursor: `next_cursor`, `has_more`. **`total` optional** — do not require exact totals. Admin may use page+total where useful. Search may use Typesense pagination. ### Idempotency See `23-idempotency.md`. ================================================================================ FILE: docs/architecture/EVENT_CATALOG.md ================================================================================ ## EVENT_CATALOG.md — v1.2 Payloads are explicit schemas. Prefer `*_public_id` for aggregates with public IDs. Internal BIGINT refs allowed only for internal consumers and **never** in external webhooks. Global: `event_id` (ULID), `correlation_id`, `occurred_at`. ### Catalog - `product.created` / `product.updated` / `product.deleted` → Search, Audit | webhook eligible ### Inventory - `inventory.reservation.created` — payload: reservation_ref (internal id OK internal-only), order_public_id, location_public_id, variant_public_id, quantity, expires_at - `inventory.reservation.released` - `inventory.sale` — emitted at fulfillment allocation commit - `inventory.adjusted` - `incoming.reservation.created` / `incoming.received` (when pre-arrival enabled) ### Orders - `order.placed` — async non-critical consumers only - `order.confirmed` — after payment captured; triggers fulfillment creation consumers - `order.completed` / `order.cancelled` ### Payments - `payment.authorized` / `payment.captured` / `payment.failed` (attempt-level; does not imply order cancel) / `payment.refunded` ### Fulfillment / Shipment - `fulfillment.created` / `fulfillment.allocated` / `fulfillment.completed` / `fulfillment.picked_up` / `fulfillment.cancelled` - `shipment.created` / `shipment.shipped` / `shipment.delivered` / `shipment.exception` / `shipment.failed` ### Vendors - `vendor.sync.completed` / `vendor.sync.failed` ### Identity - `customer.registered` All async via outbox; consumers idempotent by `event_id`. ================================================================================ FILE: docs/architecture/ERD.md ================================================================================ ## ERD.md — v1.2 Canonical counts: `TABLE_REGISTRY.md` (48 migrate / 36 foundation / 10 deferred). ### Ownership highlights - Store → `inventory_locations` - Inventory → levels, transactions, reservations, incoming* - Fulfillment → fulfillments, shipments, tracking - Shipping → methods/rates/providers (config) ### Master (migrated core) ```mermaid erDiagram CUSTOMERS ||--o{ ORDERS : places CUSTOMERS ||--o{ CARTS : owns PRODUCTS ||--o{ PRODUCT_VARIANTS : has STORES ||--o{ INVENTORY_LOCATIONS : has INVENTORY_LOCATIONS ||--o{ INVENTORY_LEVELS : tracks PRODUCT_VARIANTS ||--o{ INVENTORY_LEVELS : stocked_as INVENTORY_LEVELS ||--o{ INVENTORY_RESERVATIONS : reserved INVENTORY_LEVELS ||--o{ INVENTORY_TRANSACTIONS : ledger ORDERS ||--o{ ORDER_ITEMS : contains ORDERS ||--o{ PAYMENTS : has ORDERS ||--o{ FULFILLMENTS : fulfilled_by FULFILLMENTS ||--o{ SHIPMENTS : ships FULFILLMENTS }o--|| INVENTORY_LOCATIONS : location_fk PRODUCT_VARIANTS ||--o{ PRICES : priced ``` ### Fulfillment source RI `fulfillments.inventory_location_id` XOR `fulfillments.vendor_id` (vendor FK when vendor tables exist). ### Pre-arrival (foundation) `incoming_inventory` 1—N `incoming_inventory_reservations` ================================================================================ FILE: docs/architecture/INTEGRATION_CONTRACTS.md ================================================================================ ## INTEGRATION_CONTRACTS.md — v1.2 Business modules depend on contracts, not SDKs. Laravel 13 container bindings. ### VendorCapabilities supportsCatalogFetch, supportsAvailabilitySync, supportsReservation, supportsOrderPlacement, supportsCancellation, supportsTracking, supportsRealtimePricing. ### PaymentGateway authorize, capture, refund, void, verifyWebhook, processWebhook — with idempotency keys; timeouts → indeterminate reconcile. ### Shipping ShippingRateProvider.quote; ShippingProvider create/cancel/track/labels; optional same-day/local capabilities. ### Others TaxProvider, AgeVerificationProvider, SearchProvider, StorageProvider, NotificationProvider. Provider-specific metadata remains adapter/integration-owned. ================================================================================ FILE: docs/architecture/decisions/ADR-001-modular-monolith.md ================================================================================ # ADR-001: Modular Monolith ## Status Accepted (Architecture v1.1) ## Context The platform must support multi-store inventory, vendor integrations, payments, fulfillment, compliance, and future scale without premature microservice complexity. ## Decision Adopt a **modular monolith** in a single Laravel application with strict domain module boundaries under `app/Modules/`. Modules communicate via contracts, application actions, and domain events (transactional outbox). No microservices in Phase 1. ## Alternatives Considered | Alternative | Why rejected | |---|---| | Microservices from day 1 | Operational overhead; premature for single-store launch | | Flat Laravel structure | No enforceable boundaries; cross-module coupling risk | | Separate services per integration | Violates prompt; adds infrastructure without concrete need | ## Consequences - Faster development and simpler deployment for Phase 1. - Module boundaries must be enforced by convention, code review, and dependency rules. - Extraction to services later requires clear module contracts (already designed). ## Risks - Boundaries erode over time without discipline. - Shared database can tempt cross-module direct table access. ## Future Migration Path - Extract high-load modules (Search projection workers, Vendor sync workers) to standalone processes while keeping canonical DB. - Promote module to service only when scale, team size, or deployment independence justifies the cost. ================================================================================ FILE: docs/architecture/decisions/ADR-002-identifier-strategy.md ================================================================================ # ADR-002: Identifier Strategy ## Status Accepted (Architecture v1.1) ## Context Public APIs must not expose sequential database IDs. Internal relational joins require efficient FK performance. ## Decision - **Internal PK**: `BIGINT UNSIGNED` auto-increment on all tables. - **Public ID**: `CHAR(26)` ULID stored in `public_id` column, unique indexed. - **Foreign keys**: always reference internal `BIGINT` PKs. - **APIs**: expose only `public_id` (ULID) for business resources. ### Tables requiring `public_id` | Module | Tables | |---|---| | Identity | `users`, `customers` | | Catalog | `products`, `product_variants`, `brands`, `categories`, `collections` | | Store | `stores`, `inventory_locations` | | Orders | `orders`, `fulfillments`, `shipments` | | Payments | `payments`, `refunds` | | Vendors | `vendors`, `vendor_accounts` | | Integrations | `integrations` | | CMS | `pages`, `blog_posts` | Internal-only tables (no public API exposure): `inventory_levels`, `inventory_transactions`, `inventory_reservations`, `order_items`, `payment_attempts`, `payment_transactions`, `idempotency_keys`, `outbox_messages`, pivot/junction tables. ### ULID index strategy - Unique index on `public_id` for every table that has one. - ULIDs are generated in application layer (Laravel `Str::ulid()`). - ULID is time-sortable, 26 chars, URL-safe. ## Alternatives Considered | Alternative | Why rejected | |---|---| | UUIDv7 as PK | Larger indexes on all FKs; worse join performance | | ULID as PK everywhere | Same index bloat concern | | Sequential ID in APIs | Guessable; information leakage | ## Consequences - Every API lookup requires `public_id` → internal `id` resolution (cached where hot). - Migration/seeding must generate ULIDs consistently. - Admin and storefront URLs use ULIDs. ## Risks - Dual-identifier lookups add one indexed query per resource fetch. - Developers may accidentally expose internal IDs in API responses. ## Future Migration Path - If distributed ID generation is needed, ULID generation can move to a dedicated service without changing the column strategy. ================================================================================ FILE: docs/architecture/decisions/ADR-003-inventory-consistency.md ================================================================================ # ADR-003: Inventory Consistency — v1.2 Update ## Status Accepted (v1.1), **Amended v1.2** ## Amendment **SALE commit point** is fulfillment **ALLOCATED**, not payment capture. | Moment | reserved | on_hand | Reservation | Ledger | |---|---|---|---|---| | Checkout | +N | — | ACTIVE | none | | Payment captured | — | — | ACTIVE | none | | Fulfillment ALLOCATED | −N | −N | COMMITTED | SALE −N | `inventory_locations` owned by **Store** module; Inventory references them. Adjustments must not create `on_hand < reserved`; discrepancy workflow required. ## Previous v1.1 content Still applies for: computed available, FOR UPDATE, lock ordering, deadlock retry, Redis not primary correctness. ================================================================================ FILE: docs/architecture/decisions/ADR-004-money-representation.md ================================================================================ # ADR-004: Money Representation ## Status Accepted (Architecture v1.1) ## Context Financial data must be exact. FLOAT/DOUBLE cause rounding errors. Multiple money fields exist across pricing, orders, payments, refunds, promotions, and taxes. ## Decision Store all platform money as **integer minor units** (e.g., `$19.99` → `1999` cents). ### Schema convention | Column | Type | Example | |---|---|---| | `amount_minor` | `BIGINT` (signed for adjustments) | `1999` | | `currency_code` | `CHAR(3)` ISO 4217 | `USD` | Every table with money stores `currency_code` alongside amount columns. ### Where DECIMAL is permitted | Context | Type | Reason | |---|---|---| | Provider raw webhook payloads | `DECIMAL(19,4)` in `payment_webhook_events.raw_payload` JSON | Preserve exact provider values for reconciliation | | Vendor cost/wholesale (future) | `DECIMAL(19,4)` | Accounting precision beyond retail display | | Tax rate percentages | `DECIMAL(5,4)` | e.g., `0.0825` for 8.25% | ### Non-2-decimal currencies | Currency | Minor unit | Storage | |---|---|---| | USD, EUR, GBP | 2 decimals | multiply by 100 | | JPY, KRW | 0 decimals | store as-is (1 yen = 1 minor unit) | | BHD, KWD | 3 decimals | multiply by 1000 | Application layer uses a `Money` value object that handles conversion per currency exponent. Phase 1 operates in a single currency (USD assumed; confirm with client). ### Consistency rule All money in orders, order_items, payments, refunds, prices, promotion discounts, and tax lines use the same `amount_minor` + `currency_code` pattern. ## Alternatives Considered | Alternative | Why rejected | |---|---| | DECIMAL everywhere | Slower arithmetic; provider SDK mismatches | | FLOAT/DOUBLE | Rounding errors in financial calculations | | Store as string | No arithmetic; parsing overhead | ## Consequences - All API responses convert minor units to display format at the presentation layer. - Provider adapters must convert between provider formats and internal minor units. - Sum/aggregate queries use integer arithmetic (exact). ## Risks - Developer error converting between display and minor units. - Multi-currency requires exchange rate tables (deferred). ## Future Migration Path - Add `exchange_rates` table and `Money::convert()` when multi-currency is required. - DECIMAL columns for accounting exports can be generated views, not canonical storage. ================================================================================ FILE: docs/architecture/decisions/ADR-005-transactional-outbox.md ================================================================================ # ADR-005: Transactional Outbox — v1.2 Update ## Status Accepted (v1.1), **Amended v1.2** ## Amendment 1. Claim with `FOR UPDATE SKIP LOCKED`. 2. `PROCESSED` means successful **enqueue to Redis** (handoff), not completion of all consumers. 3. Never hold DB locks during external side effects. 4. At-least-once delivery; consumers idempotent by `event_id`. Retry/backoff/stuck recovery from v1.1 remain. ================================================================================ FILE: docs/architecture/decisions/ADR-006-provider-adapter-architecture.md ================================================================================ # ADR-006: Provider Adapter Architecture ## Status Accepted (Architecture v1.1) ## Context The platform integrates with vendors (SGProof), payment gateways (Stripe), shipping carriers (UPS/FedEx), tax providers, age verification, search (Typesense), and storage (S3). External providers must not dictate core database architecture. ## Decision All third-party integrations use **contract interfaces** with **concrete adapter implementations** registered via Laravel service container. ### Contract categories | Contract | Examples | |---|---| | `VendorConnector` | SGProofConnector, ApiVendorConnector, CsvVendorConnector | | `PaymentGateway` | StripeGateway, PayPalGateway | | `ShippingRateProvider` | UpsRateProvider, FedExRateProvider | | `ShippingProvider` | UpsShipmentProvider, LocalDeliveryProvider | | `TaxProvider` | TaxJarProvider, ManualTaxProvider | | `AgeVerificationProvider` | (future) | | `SearchProvider` | TypesenseProvider | | `StorageProvider` | S3StorageProvider | | `NotificationProvider` | SesNotificationProvider, TwilioNotificationProvider | ### Rules 1. Business modules depend on contracts, never concrete SDKs. 2. Provider-specific data stored in adapter-owned staging tables (`vendor_products`, `payment_webhook_events`), never in core domain tables. 3. Credentials encrypted at rest; never logged. 4. Each adapter implements `healthCheck()` for monitoring. 5. Capability interfaces used where providers cannot support all operations. ### Integration registry tables - `integrations` — provider type, status, config - `integration_credentials` — encrypted secrets - `integration_settings` — non-secret config - `integration_logs` — operation audit trail ## Alternatives Considered | Alternative | Why rejected | |---|---| | Direct SDK calls in services | Tight coupling; untestable; provider dictates schema | | Plugin system with dynamic loading | Over-engineered for Phase 1 | ## Consequences - New providers added by implementing contract + registering in container. - Mock adapters enable full test coverage without external dependencies. ## Risks - Contract design may not fit all future providers; may need capability splits. - Adapter maintenance burden grows with provider count. ## Future Migration Path - Extract high-traffic adapters (vendor sync) to standalone worker processes. - Add adapter versioning if provider APIs change frequently. ================================================================================ FILE: docs/architecture/decisions/ADR-007-search-projection.md ================================================================================ # ADR-007: Search Projection (Typesense) ## Status Accepted (Architecture v1.1) ## Context Product search requires fast full-text and faceted queries. MySQL is the canonical source of truth. Typesense is approved as the search engine. ## Decision Typesense is a **read-only projection** of catalog and availability data. It is never authoritative. ### Index flow ``` MySQL state change → domain event written to outbox (same transaction) → outbox worker dispatches SearchIndexJob to Laravel Queue → SearchIndexJob upserts/deletes document in Typesense ``` ### Operations | Operation | Trigger | Action | |---|---|---| | Create/Update | `product.created`, `product.updated`, `inventory.adjusted` | Upsert document | | Delete | `product.deleted` (soft) | Remove or mark inactive in index | | Full rebuild | Admin command or schema version change | Rebuild collection from MySQL | ### Failure handling - Typesense failure does NOT roll back or affect MySQL transactions. - Failed indexing jobs retry via queue (max 5 attempts). - `search_index_state` table tracks last-indexed version per entity. - Admin dashboard shows indexing lag. ### Schema versioning - Collection name includes schema version: `products_v1`, `products_v2`. - Rebuild creates new collection, populates it, then swaps alias. - Alias swap (`products` → `products_v2`) enables zero-downtime rebuild. - Old collection deleted after successful swap. ### Phase 1 scope - Architectural strategy only. Exact field/facet definitions deferred until Catalog schema is approved. - Index: product name, variant SKU, brand, category, price range, availability flag. - Facets: category, brand, alcohol type (when catalog attributes are finalized). ## Alternatives Considered | Alternative | Why rejected | |---|---| | MySQL full-text search | Poor faceting; doesn't scale for complex liquor attributes | | Elasticsearch | Not approved infrastructure | | Typesense as source of truth | Violates canonical data principle | ## Consequences - Search results may be briefly stale (eventual consistency, typically < 5 seconds). - Rebuild capability is mandatory for disaster recovery. ## Risks - Schema changes require rebuild or migration strategy. - Availability in search index may lag behind real-time inventory. ## Future Migration Path - Location-aware availability in search index (computed projection). - Separate collections per store if multi-store search isolation is needed. ================================================================================ FILE: docs/architecture/decisions/ADR-008-order-payment-fulfillment-separation.md ================================================================================ # ADR-008: Order/Payment/Fulfillment Separation — v1.2 Update ## Status Accepted (v1.1), **Amended v1.2** ## Amendments 1. Payment attempt failure does not auto-cancel order. 2. Order COMPLETED is quantity/outcome based, not “all fulfillments terminal”. 3. Pickup lives on fulfillment (READY_FOR_PICKUP / PICKED_UP); no fake shipment. 4. Fulfillment owns shipment rows; Shipping owns provider orchestration. 5. Inventory SALE commits at fulfillment ALLOCATED. 6. Fulfillment sources use typed FKs (`inventory_location_id` / `vendor_id`). ================================================================================ FILE: docs/architecture/decisions/ADR-009-api-versioning-and-contracts.md ================================================================================ # ADR-009: API Versioning and Contracts ## Status Accepted (Architecture v1.1) ## Context The platform serves a customer storefront (Next.js), Super Admin (Next.js), and future automation/AI agents. APIs must be stable, versioned, and contract-documented from day one. ## Decision ### Versioning - All APIs under `/api/v1/` from launch. - Breaking changes require `/api/v2/` with deprecation period for v1. - Version in URL path, not headers. ### Response contract - Never expose Eloquent models directly. - Use explicit API Resources/DTOs. - Consistent envelope: `{ data, meta, request_id, correlation_id }`. - Consistent error envelope: `{ error: { code, message, details }, request_id }`. ### Error categories (stable codes) `VALIDATION_ERROR`, `AUTHENTICATION_REQUIRED`, `FORBIDDEN`, `RESOURCE_NOT_FOUND`, `CONFLICT`, `INVENTORY_UNAVAILABLE`, `INVALID_STATE_TRANSITION`, `IDEMPOTENCY_CONFLICT`, `RATE_LIMITED`, `INTEGRATION_FAILURE`, `INTERNAL_ERROR`. ### Idempotency Critical write endpoints require `Idempotency-Key` header. Scope: actor + route + request fingerprint. ### OpenAPI - OpenAPI 3.x spec generated and stored in-repo as first-class artifact. - Contract tests validate API responses against spec. ### Public identifiers APIs expose ULID `public_id` only. Internal BIGINT PKs never appear in responses. ## Alternatives Considered | Alternative | Why rejected | |---|---| | Header-based versioning | Harder to route; less visible | | GraphQL | Not approved; adds complexity | | No versioning from launch | Breaking changes become painful | ## Consequences - Every endpoint change requires spec update. - DTO layer adds boilerplate but ensures stability. ## Risks - Spec drift if not enforced in CI. - Over-versioning if v1 changes are too frequent. ## Future Migration Path - Automated OpenAPI generation from route definitions + DTO annotations. - MCP/AI agent layer wraps existing v1 APIs without rewriting business logic. ================================================================================ FILE: docs/architecture/decisions/ADR-010-checkout-transaction-strategy.md ================================================================================ # ADR-010: Checkout Application Transaction Strategy ## Status Accepted (Architecture v1.2) ## Context Strict module ownership forbids Checkout mutating all tables, yet order placement requires atomic reserve + order create. ## Decision Checkout is an orchestrator that opens an outer MySQL transaction and invokes module actions (ReservationService, OrderService, OutboxPublisher) on the same connection. Each action enforces its module invariants. External provider calls occur only after COMMIT. ## Alternatives | Alt | Rejected because | |---|---| | Eventual reserve after order | Oversell / unpaid order without stock | | Checkout updates all tables | Breaks ownership | | Distributed 2PC | Over-engineered | ## Consequences Clear sync vs async boundary; testable module actions; no network in DB TX. ================================================================================ FILE: docs/architecture/decisions/ADR-011-phase1-schema-minimalism.md ================================================================================ # ADR-011: Phase-1 Schema Minimalism ## Status Accepted (Architecture v1.2) ## Context v1.1 planned migrating ~28 foundation tables “just in case”. ## Decision Migrate only **REQUIRED NOW** tables (TABLE_REGISTRY). Architectural foundation remains design/contracts until feature approval. Exception only if postponing causes destructive redesign of a required table (none currently beyond stores/locations already required). ## Consequences Smaller initial schema; feature flags gate later migrations; less speculative complexity. ================================================================================ END OF ARCHITECTURE V1.2 COMPLETE DUMP ================================================================================