Date: 2026-04-29
Goal: Align product intake and receiving flows with Shopify/Stripe industry standards; improve UX for non-technical users.
Files: src/domain/models.ts
Productmodel:id,name,sku,price,cost,taxCode,image,description,isActive,isDigital,createdAt,updatedAt.- No
ProductStatusenum (draft | active | archived). - No
barcode/upcfield. - No
vendor/supplier/brandfield. - No
tagsfield. - No
productType/categoryfield. - No
seoTitle,seoDescription,slugfields. - No cost-of-goods-sold history.
Variantmodel exists but onlyid,productId,sku,price,image,createdAt— no inventory fields at variant level.PurchaseOrdermodel:id,poNumber,supplier,status,orderDate,expectedDeliveryDate,notes,totalCost,createdAt,updatedAt.PurchaseOrderItem:id,poId,productId,quantity,unitCost.ReceivingStatusenum:PENDING,PARTIAL,RECEIVED,OVER_RECEIVED.- Missing:
Shipment/Receivingmodel for tracking individual receipts. - Missing:
InventoryAdjustmentmodel for ledger-style history. - Missing:
Supplier/Vendoraggregate. - Missing:
ProductMedia/Assetsupport.
Files: src/core/ProductService.ts, src/core/PurchaseOrderService.ts, src/core/TransferService.ts
ProductService: Basic CRUD + search.receiveInventorymethod exists but is called fromPurchaseOrderService.receiveAllItems. No bulk import. No cost history.PurchaseOrderService:create,update,delete,getById,getAll,receiveAllItems,transitionStatus.receiveAllItemsreceives ALL items at once immediately. No partial. No over-receive guard. No shipment tracking.
TransferService: Present but no UI entry point for receiving via transfer.
Files: src/infrastructure/sqlite/schema.ts
- Tables:
products,product_variants,inventory_levels,purchase_orders,purchase_order_items. - No
shipments/receivingstable. - No
supplierstable. - No
product_tagstable. - No
product_mediatable. - No
inventory_adjustments(ledger) table. - No
barcodeindex on products.
Files: src/ui/pages/admin/AdminProducts.tsx, src/ui/pages/admin/AdminProductForm.tsx, src/ui/pages/admin/AdminPurchaseOrders.tsx, src/ui/navigation/adminNavigation.ts
AdminProducts: Fixed 50-item table, no pagination, no filtering, no status badges, no bulk actions, no CSV export/import.AdminProductForm: Single-page form with name, SKU, price, cost, taxCode, image URL, description, toggle. No tabs, no variants editor, no SEO fields, no barcode field, no vendor field.AdminPurchaseOrders: Table with no detail page. receiveAndUpdateInventory button for ALL items immediately. No purchase order creation UI. No draft/pending etc workflow. No receiving detail view.adminNavigation.ts: Settings link broken ("#"). Transfer link points to#settings/team(wrong). No suppliers/vendor link.
Files: src/app/api/admin/products/route.ts, src/app/api/admin/purchase-orders/route.ts
- Products API: GET (list/search), POST (create), PUT (update), DELETE. No bulk import endpoint. No barcode lookup.
- Purchase Orders API: GET (list), POST (create), PUT (update), DELETE. No receive endpoint. No partial receive endpoint. No PO detail endpoint.
-
Product Intake & Catalog Management
- Products must support a status lifecycle: Draft → Active → Archived.
- Products need barcode/UPC fields for scanning and lookup.
- Products must have vendor, tags, product type, and SEO fields (title, description, slug).
- Products need a compare-at price (MSRP) for sale display.
- Bulk product import via CSV with progress tracking and per-row error reporting.
-
Supplier Management
- Suppliers must be first-class entities with contact info (name, email, phone, address).
- Purchase orders must link to a supplier by ID, not just a string.
- Suppliers must be editable and deletable.
-
Purchase Order Receiving
- Partial receiving must be supported (receive a subset of items or quantities).
- Over-receive protection must prevent exceeding ordered quantity by more than 10%.
- Each receiving event must be tracked: who received it, when, tracking number, condition, notes.
- Receiving must update inventory atomically and record an audit trail.
-
Inventory Ledger (Audit Trail)
- Every inventory change must be recorded with before/after quantities, delta, reason, and user.
- The ledger must record reference to the source (PO, transfer, manual, order, receiving).
- Ledger must be queriable by product, location, or reference.
-
UX / Navigation
- Product list must have pagination, filtering, sorting, search, bulk actions, and status badges.
- Product creation must be a tabbed wizard (Details, Variants, SEO, Inventory).
- Purchase orders must have detail pages with items and receiving history.
- Receiving UI must be a focused, step-by-step flow (barcode → quantity → confirm).
- Admin navigation must be restructured into logical groups: Products, Purchasing, Analytics.
- Broken navigation links (Settings → "#", Transfers → wrong href) must be fixed.
- Backward Compatibility: All schema changes must be additive with DEFAULT values. No breaking API contract changes.
- Transaction Safety: Receiving operations must be atomic across inventory, adjustments, receiving items, and PO status.
- Auditability: Inventory adjustments table must exist before any receiving UI ships.
- Performance: New queries must use indices. No full-table scans.
- Maintainability: JoyZoning purity must be preserved. Domain has zero external imports. UI never holds business logic.
| Pattern | Shopify Implementation | Our Current State | Gap Severity |
|---|---|---|---|
| Product Status Lifecycle | Draft → Active → Archived | Single isActive boolean |
HIGH |
| Barcode / UPC | barcode field on Product/Variant |
None | HIGH |
| Vendor / Supplier | vendor field + full Suppliers app |
supplier string on PO only |
HIGH |
| Product Tags | Comma-separated tags, searchable | None | MEDIUM |
| Product Type | productType for categorization |
None | MEDIUM |
| SEO Fields | Title, description, URL handle | None | MEDIUM |
| Cost Tracking | Cost per item, margin calc | cost field only |
MEDIUM |
| Variants | Multi-option (Size, Color, etc.) | Schema exists, no UI | HIGH |
| Bulk Import | CSV import with mapping wizard | None | HIGH |
| Product Media | Image gallery, video, 3D | Single image string |
MEDIUM |
| Collections | Manual & automated collections | None | LOW |
| Pattern | Shopify Implementation | Our Current State | Gap Severity |
|---|---|---|---|
| Purchase Order Detail | Full PO detail page with items | No detail page | HIGH |
| Partial Receiving | Receive items in batches | receiveAllItems only (all at once) |
HIGH |
| Over-Receive Protection | Configurable over-receive limit | None | MEDIUM |
| Shipment Tracking | Tracking #, carrier, shipped date | None | MEDIUM |
| Unit Cost at Receive | Capture actual cost during receive | Static unitCost from PO |
MEDIUM |
| Receiving Notes | Notes per receiving event | None | LOW |
| Inventory Ledger | Every change tracked with reason | No history table | HIGH |
| Adjustments | Dedicated adjustment flow with reason | No UI, no model | HIGH |
| Bin / Location | Inventory at location + bin | Only inventory_levels with locationId |
MEDIUM |
| Stock Alerts | Low stock, out of stock notifications | No alerting | LOW |
| Pattern | Shopify Implementation | Our Current State | Gap Severity |
|---|---|---|---|
| Step-by-Step Product Wizard | Guided product creation | Single flat form | HIGH |
| Visual Receiving Flow | Scan barcode → quantity → confirm | Button-click receive all | HIGH |
| Filterable Data Tables | Search, filters, sort, pagination | Static 50-row table | HIGH |
| Bulk Actions | Select rows → bulk update/delete | None | MEDIUM |
| Status Badges | Color-coded status pills | Text only | MEDIUM |
| Detail Pages | Click row → full detail | No detail pages for POs or Products | HIGH |
| Breadcrumbs | Clear navigation path | None | LOW |
| Quick-Add FAB | Floating action button for create | Standard button | LOW |
Files to modify/create:
src/domain/models.ts— extend existing, add new modelssrc/domain/rules.ts— add business rules for receivingsrc/domain/repositories.ts— add new repository interfacessrc/domain/errors.ts— add receiving errors
Pure business logic additions:
// New enums
enum ProductStatus { DRAFT = 'DRAFT', ACTIVE = 'ACTIVE', ARCHIVED = 'ARCHIVED' }
enum AdjustmentReason { PURCHASE = 'PURCHASE', RETURN = 'RETURN', DAMAGE = 'DAMAGE', CORRECTION = 'CORRECTION' }
// Extended Product
interface Product {
...
status: ProductStatus; // DRAFT/ACTIVE/ARCHIVED
barcode?: string; // UPC/EAN/ISBN
vendor?: string; // Supplier/vendor name
tags: string[]; // Product tags
productType?: string; // Category/type
seoTitle?: string;
seoDescription?: string;
slug?: string; // URL-friendly handle
compareAtPrice?: number; // MSRP for sale display
weight?: number; // For shipping calc
weightUnit?: 'g' | 'kg' | 'lb' | 'oz';
}
// New: Supplier Aggregate
interface Supplier {
id: string;
name: string;
contactName?: string;
email?: string;
phone?: string;
address?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Extended PurchaseOrder
interface PurchaseOrder {
...
supplierId?: string; // FK to Supplier
trackingNumber?: string; // Shipment tracking
shippingCarrier?: string;
shippingCost?: number;
taxCost?: number;
discount?: number;
}
// New: Receiving (Shipment) Aggregate
interface Receiving {
id: string;
poId: string;
receivedAt: string;
receivedBy?: string; // User ID
trackingNumber?: string;
notes?: string;
createdAt: string;
}
interface ReceivingItem {
id: string;
receivingId: string;
poItemId: string;
productId: string;
quantityReceived: number;
unitCost: number; // Actual cost at receive time
condition: 'NEW' | 'DAMAGED' | 'DEFECTIVE';
notes?: string;
}
// New: Inventory Adjustment (Ledger)
interface InventoryAdjustment {
id: string;
productId: string;
locationId: string;
variantId?: string;
quantityBefore: number;
quantityAfter: number;
delta: number;
reason: AdjustmentReason;
referenceType: 'PURCHASE_ORDER' | 'TRANSFER' | 'MANUAL' | 'ORDER' | 'RECEIVING';
referenceId?: string;
notes?: string;
createdAt: string;
createdBy?: string;
}
// New: Bulk Import Job
interface ImportJob {
id: string;
type: 'PRODUCTS' | 'INVENTORY';
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
fileName: string;
totalRows: number;
processedRows: number;
errorRows: number;
errors: ImportError[];
createdAt: string;
completedAt?: string;
}Business Rules (src/domain/rules.ts):
function canReceiveItem(poItem: PurchaseOrderItem, alreadyReceived: number, qtyToReceive: number): boolean {
// Prevent over-receiving beyond a configurable threshold (default 110%)
const threshold = 1.1;
return (alreadyReceived + qtyToReceive) <= (poItem.quantity * threshold);
}
function calculatePoReceivedStatus(items: { ordered: number; received: number }[]): ReceivingStatus {
const totalOrdered = items.reduce((s, i) => s + i.ordered, 0);
const totalReceived = items.reduce((s, i) => s + i.received, 0);
if (totalReceived === 0) return 'PENDING';
if (totalReceived >= totalOrdered) return 'RECEIVED';
return 'PARTIAL';
}Repository Interfaces to Add (src/domain/repositories.ts):
interface ISupplierRepository { create, getById, getAll, update, delete, getByName }
interface IReceivingRepository { createReceiving, getReceivingById, getReceivingsByPo, getReceivingItems, deleteReceiving }
interface IInventoryAdjustmentRepository { recordAdjustment, getAdjustmentsByProduct, getAdjustmentsByLocation, getInventoryHistory }
interface IImportJobRepository { create, update, getById, getAll, delete }Files to modify/create:
src/core/ProductService.ts— add bulk import, barcode lookup, status transitionssrc/core/PurchaseOrderService.ts— refactor receiving to support partial, add shipment trackingsrc/core/SupplierService.ts— new servicesrc/core/ReceivingService.ts— new service for receiving orchestrationsrc/core/InventoryAdjustmentService.ts— new service for ledger operationssrc/core/ImportService.ts— new service for CSV import
Key Logic:
ReceivingService.receiveItems(poId, items[]):- Validate PO exists and is APPROVED or PARTIAL.
- For each item: validate quantity against ordered + threshold.
- Create Receiving record + ReceivingItem records.
- Update inventory levels.
- Record InventoryAdjustment entries.
- Update PO received quantities and status.
- All in a transaction.
ImportService.processProducts(file):- Parse CSV.
- Validate rows using domain rules.
- Create products in batches.
- Update ImportJob status.
Files to modify/create:
src/infrastructure/sqlite/schema.ts— add new tablessrc/infrastructure/repositories/sqlite/SQLiteSupplierRepository.ts— newsrc/infrastructure/repositories/sqlite/SQLiteReceivingRepository.ts— newsrc/infrastructure/repositories/sqlite/SQLiteInventoryAdjustmentRepository.ts— newsrc/infrastructure/repositories/sqlite/SQLiteImportJobRepository.ts— newsrc/infrastructure/services/CsvParser.ts— CSV parsing utilitysrc/app/api/admin/suppliers/— API routessrc/app/api/admin/receiving/— API routessrc/app/api/admin/import/— API routes
Schema Additions:
-- Suppliers table
CREATE TABLE suppliers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
contact_name TEXT,
email TEXT,
phone TEXT,
address TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Product additions (via ALTER or migration)
ALTER TABLE products ADD COLUMN status TEXT DEFAULT 'ACTIVE';
ALTER TABLE products ADD COLUMN barcode TEXT;
ALTER TABLE products ADD COLUMN vendor TEXT;
ALTER TABLE products ADD COLUMN tags TEXT; -- JSON array
ALTER TABLE products ADD COLUMN product_type TEXT;
ALTER TABLE products ADD COLUMN seo_title TEXT;
ALTER TABLE products ADD COLUMN seo_description TEXT;
ALTER TABLE products ADD COLUMN slug TEXT;
ALTER TABLE products ADD COLUMN compare_at_price REAL;
ALTER TABLE products ADD COLUMN weight REAL;
ALTER TABLE products ADD COLUMN weight_unit TEXT;
-- Receivings table
CREATE TABLE receivings (
id TEXT PRIMARY KEY,
po_id TEXT NOT NULL REFERENCES purchase_orders(id),
received_at TEXT NOT NULL,
received_by TEXT,
tracking_number TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Receiving items
CREATE TABLE receiving_items (
id TEXT PRIMARY KEY,
receiving_id TEXT NOT NULL REFERENCES receivings(id),
po_item_id TEXT NOT NULL REFERENCES purchase_order_items(id),
product_id TEXT NOT NULL REFERENCES products(id),
quantity_received INTEGER NOT NULL,
unit_cost REAL,
condition TEXT DEFAULT 'NEW',
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Inventory adjustments (ledger)
CREATE TABLE inventory_adjustments (
id TEXT PRIMARY KEY,
product_id TEXT NOT NULL REFERENCES products(id),
location_id TEXT NOT NULL REFERENCES inventory_locations(id),
variant_id TEXT REFERENCES product_variants(id),
quantity_before INTEGER NOT NULL,
quantity_after INTEGER NOT NULL,
delta INTEGER NOT NULL,
reason TEXT NOT NULL,
reference_type TEXT,
reference_id TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
created_by TEXT
);
-- Create indices
CREATE INDEX idx_products_barcode ON products(barcode);
CREATE INDEX idx_products_vendor ON products(vendor);
CREATE INDEX idx_products_status ON products(status);
CREATE INDEX idx_products_slug ON products(slug);
CREATE INDEX idx_receivings_po ON receivings(po_id);
CREATE INDEX idx_receiving_items_receiving ON receiving_items(receiving_id);
CREATE INDEX idx_inventory_adjustments_product ON inventory_adjustments(product_id);
CREATE INDEX idx_inventory_adjustments_reference ON inventory_adjustments(reference_type, reference_id);Files to modify/create:
src/ui/navigation/adminNavigation.ts— fix broken links, add Supplierssrc/ui/pages/admin/AdminProducts.tsx— pagination, filters, status badges, bulk actions, import buttonsrc/ui/pages/admin/AdminProductForm.tsx— tabbed wizard (Details, Variants, SEO, Media), barcode field, vendor fieldsrc/ui/pages/admin/AdminProductDetail.tsx— new: full product detail pagesrc/ui/pages/admin/AdminSuppliers.tsx— new: supplier listsrc/ui/pages/admin/AdminSupplierForm.tsx— new: supplier create/editsrc/ui/pages/admin/AdminPurchaseOrders.tsx— status filters, click-through to detailsrc/ui/pages/admin/AdminPurchaseOrderDetail.tsx— new: full PO detail with items, receivings history, receive actionsrc/ui/pages/admin/AdminReceiving.tsx— new: barcode-scan receiving flowsrc/ui/pages/admin/AdminInventoryAdjustments.tsx— new: inventory history/ledger viewsrc/ui/components/admin/BulkActionsBar.tsx— new: reusable bulk action barsrc/ui/components/admin/StatusBadge.tsx— new: color-coded status badgessrc/ui/components/admin/BarcodeScanner.tsx— new: camera/barcode inputsrc/ui/components/admin/ImportModal.tsx— new: CSV import with preview
Navigation Structure (mirroring Shopify admin):
Products
├── All Products (AdminProducts)
├── Add Product (AdminProductForm)
├── Inventory (AdminInventory → link to adjustments)
└── Import (ImportModal on Products page)
Purchasing
├── Purchase Orders (AdminPurchaseOrders)
├── Suppliers (AdminSuppliers)
└── Receiving (AdminReceiving)
Analytics
├── Overview
├── Inventory...
Files to create:
src/app/api/admin/suppliers/route.ts— CRUDsrc/app/api/admin/suppliers/[id]/route.ts— CRUDsrc/app/api/admin/purchase-orders/[id]/route.ts— GET detail, PUT, DELETEsrc/app/api/admin/purchase-orders/[id]/receive/route.ts— POST: partial receivesrc/app/api/admin/receiving/route.ts— GET list, POST createsrc/app/api/admin/receiving/[id]/route.ts— GET detail, DELETEsrc/app/api/admin/inventory/adjustments/route.ts— GET ledger, POST manual adjustmentsrc/app/api/admin/products/import/route.ts— POST CSV uploadsrc/app/api/admin/products/import/[id]/route.ts— GET job status
- Add
ProductStatus,barcode,vendor,tags,productType,seoTitle,seoDescription,slug,compareAtPriceto Product model. - Add
Suppliermodel. - Add
Receiving,ReceivingItem,InventoryAdjustmentmodels. - Update SQLite schema with migrations.
- Create repository interfaces.
- Add domain business rules for receiving.
- Extend
ProductServicewith bulk import support, barcode lookup. - Create
SupplierService. - Create
ReceivingServicewith transaction-safe partial receiving. - Create
InventoryAdjustmentServicefor ledger. - Create
ImportServicewith CSV parsing. - Refactor
PurchaseOrderServiceto use ReceivingService.
- Implement
SQLiteSupplierRepository. - Implement
SQLiteReceivingRepository. - Implement
SQLiteInventoryAdjustmentRepository. - Implement
SQLiteImportJobRepository. - Wire up in DI container.
- Create API routes.
- Redesign
AdminProductswith filters, pagination, status badges, bulk actions. - Redesign
AdminProductFormas tabbed wizard (Details, SEO, Variants). - Create
AdminProductDetailpage. - Add Import modal to Products page.
- Add barcode field.
- Create
AdminSupplierslist and form. - Fix
AdminPurchaseOrderswith status filters. - Create
AdminPurchaseOrderDetailwith items + receiving history + receive action. - Create
AdminReceivingpage with barcode scan flow. - Update admin navigation (fix broken links, add Suppliers group).
- Create
AdminInventoryAdjustmentsview. - Add adjustment history to product detail.
- Add low-stock indicators.
- Cross-layer dependency check: Domain gets zero new external imports. All new I/O (CSV parsing, DB) stays in Infrastructure. Core orchestrates only. ✅
- Interface contracts: Every new repository gets a domain interface before SQLite implementation. ✅
- Transaction safety: Receiving must be atomic (inventory + adjustments + PO status). SQLite transactions used. ✅
- Backward compatibility: Existing
products,purchase_orderstables getDEFAULTvalues for new columns. No breaking changes. ✅ - JoyZoning purity: Domain models are pure value objects. No UI state, no I/O. ✅
- "Is this over-engineered?" — Receiving and ledger are table stakes for any real inventory system. Shopify has all of this. The current "receive all at once" is fragile and loses audit history. The plan adds necessary observability.
- "What about performance?" — SQLite will handle this scale fine. Indices on barcode, status, and reference_type ensure lookups remain fast.
- "Will the CSV import block the server?" — Should be processed in chunks (25 rows at a time) with streaming CSV parse. ImportJob tracks progress asynchronously.
- "Is the tabbed form discoverable?" — Using familiar Shopify tabs: "Details | Variants | SEO | Inventory". Non-technical users know this pattern.
- "What about barcode scanning?" — Start with manual text input with a camera icon. Native Barcode Detection API as progressive enhancement later.
- Migration safety: Schema changes use
ALTER TABLE ADD COLUMNwhich is safe in SQLite. New tables are independent. Existing data untouched. - Data integrity: All receiving operations wrapped in
BEGIN TRANSACTION/COMMIT. Rollback on any failure. - Audit trail: Every inventory change recorded in
inventory_adjustmentswithcreated_byuser tracking. - Error handling: CSV import produces detailed
ImportError[]per row. Users can fix and retry. - Rollback plan: If issues arise, reverting requires only removing new API routes and UI pages. Domain models and DB schema are additive only.
| Decision | Choice | Rationale |
|---|---|---|
| Partial receiving | Yes, mandatory | Industry standard. Receiving all at once is unrealistic. |
| Inventory ledger | Separate inventory_adjustments table |
Audit trail required for any production system. |
| Supplier aggregate | First-class entity, not just string | Enables supplier reporting, contact info, multiple POs per supplier. |
| Product status | Enum over boolean | Draft state needed for creating before publishing. Archived hides from storefront without deleting. |
| Barcode | Text field on Product + Variant | UPC/EAN can be 13-14 digits. No validation regex needed (varies globally). |
| CSV import | Server-side chunked processing | Client-side parsing is fast but lacks validation against domain rules. |
| Receiving UI | Dedicated page, not inline on PO | Non-technical users need a focused, step-by-step flow. Mirrors Shopify's "Receive inventory" action. |
| Layer | Files Changed | Files Created |
|---|---|---|
| DOMAIN | models.ts, rules.ts, errors.ts, repositories.ts |
0 |
| CORE | ProductService.ts, PurchaseOrderService.ts, container.ts |
SupplierService.ts, ReceivingService.ts, InventoryAdjustmentService.ts, ImportService.ts |
| INFRASTRUCTURE | schema.ts, database.ts, services.ts (DI) |
SQLiteSupplierRepository.ts, SQLiteReceivingRepository.ts, SQLiteInventoryAdjustmentRepository.ts, SQLiteImportJobRepository.ts, CsvParser.ts + API routes |
| UI | AdminProducts.tsx, AdminProductForm.tsx, AdminPurchaseOrders.tsx, adminNavigation.ts, AdminInventory.tsx |
AdminProductDetail.tsx, AdminSuppliers.tsx, AdminSupplierForm.tsx, AdminPurchaseOrderDetail.tsx, AdminReceiving.tsx, AdminInventoryAdjustments.tsx, StatusBadge.tsx, BulkActionsBar.tsx, BarcodeScanner.tsx, ImportModal.tsx |
| PLUMBING | 0 | csvHelpers.ts, barcodeValidator.ts |
✅ Domain remains pure. Zero external imports in Domain.
✅ Interfaces first. All new repositories defined in Domain before implementation.
✅ Dependency inversion. Core depends on Domain interfaces; Infrastructure implements them.
✅ UI renders state, never decides business outcomes. All business logic lives in Core.
✅ Additive changes only. No breaking schema changes. Existing functionality preserved.
✅ Audit trail guaranteed. Every inventory mutation is recorded.
| Risk Vector | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Schema migration failure | Low | High | All additions are ALTER TABLE ADD COLUMN with DEFAULT values. No destructive changes. Migration script included. |
| API route conflicts | Low | Medium | New routes use distinct path segments (/suppliers, /receiving, /import). No overlap with existing. |
| UI regression | Medium | Medium | Existing pages modified incrementally. New pages are additive. Feature flags not needed due to additive-only approach. |
| Performance degradation | Low | Low | New indices on high-cardinality columns (barcode, status). No full-table scans introduced. |
| Data loss during receiving | Low | Critical | All receiving operations use SQLite transactions with explicit rollback on failure. |
| Import job orphaning | Medium | Medium | ImportJob status tracking ensures jobs can be retried or cleaned up. |
Domain (models, rules, errors, repositories)
↓ (interfaces only)
Core (ProductService, PurchaseOrderService, ReceivingService, ...)
↓ (orchestration)
Infrastructure (SQLite Repos, API Routes, CsvParser)
↓ (renders)
UI (Pages, Components, Navigation)
Validation: No upward arrows. Domain imports nothing. UI imports only Domain + Plumbing. Core coordinates Domain + Infrastructure.
- Existing
Productshape preserved (new fields are optional). - Existing
PurchaseOrdershape preserved (new fields are optional). isActiveboolean co-exists with newstatusenum (statusderived fromisActivefor migration).- All existing API responses remain unchanged (new fields only appear with new request shapes).
- Immediate: Remove new API route files. Next.js will 404 them gracefully.
- Short-term: Revert UI page changes using git.
- Schema: SQLite
ALTER TABLE ADD COLUMNis irreversible by standard SQLite, but new columns are unused by old code. To fully revert, restore from backup taken before migration. - Data: No existing data is modified by migration. Full rollback = schema restore + removal of new code.
inventory_adjustmentstable becomes the canonical audit log for all inventory changes.ImportJobprovides visibility into bulk operations.- Receiving records provide traceability from PO → receipt → inventory.
- Schema integrity: All new columns added with
DEFAULTvalues. No existing column types changed. NoNOT NULLwithout default on existing rows. New tables are independent. - Domain purity verified: No imports from
core/,infrastructure/, orui/insrc/domain/. All new types are plain interfaces and enums. - Repository contracts defined before implementation:
ISupplierRepository,IReceivingRepository,IInventoryAdjustmentRepository,IImportJobRepositorywill be added tosrc/domain/repositories.tsbefore any SQLite class is written. - Migration ordering:
ALTER TABLEstatements run before newCREATE TABLEstatements.CREATE INDEXruns after table creation.
- Type safety: All new API routes will use the existing
requireAdmin()guard and validate request shapes against domain types. - Testability: Domain rules (e.g.,
canReceiveItem,calculatePoReceivedStatus) are pure functions with no side effects — unit-testable with zero mocks. - Transaction coverage audit: Every receiving path must touch
inventory_levels,inventory_adjustments,receiving_items, andpurchase_ordersunder a single SQLitetransaction()call. - Error surface: New domain errors (
OverReceiveError,InvalidReceiveStateError) will be added tosrc/domain/errors.tsand surfaced to the UI consistently via existing error wrappers.
- Rollback gate: All scheme changes are additive. Immediate rollback = delete new API routes and UI pages; functional code continues to work. Full rollback requires DB backup restore (standard practice documented).
- No breaking API changes: Existing
GET /api/admin/productsandGET /api/admin/purchase-ordersresponses remain identical. New fields only appear when new request shapes are used. - Feature flags not needed: Additive-only changes mean old and new code paths coexist safely.
- Observability before shipping:
inventory_adjustmentstable must be in place before any receiving UI goes live, otherwise audit history gaps occur.
This plan is ready for implementation. The user should review and then toggle to Act mode to begin implementation. Recommended starting point: Phase 1 (Domain & Schema).