From 3712600c4ed6103e981f0669e2f247940fcf6864 Mon Sep 17 00:00:00 2001 From: kevin-asprec Date: Thu, 5 Mar 2026 10:17:28 +0800 Subject: [PATCH] feat(04-01): InventoryItem and StockMovement schema with enums - Add ItemTrackingType (SERIALIZED/BATCH), ItemCondition, MovementType, LocationType enums - Add InventoryItem model with dual tracking (serialized by serial number, batch by quantity) - Add StockMovement model as immutable movement ledger (no updatedAt) - Add recordedMovements relation on User model - Indexes on tenantId, itemType, trackingType, inventoryItemId, movementType Co-Authored-By: Claude Opus 4.6 --- prisma/schema.prisma | 98 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68427d7..1c84923 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -142,6 +142,32 @@ enum CompensationModel { HYBRID } +enum ItemTrackingType { + SERIALIZED + BATCH +} + +enum ItemCondition { + NEW + REFURBISHED + USED + DAMAGED +} + +enum MovementType { + RECEIVED + ISSUED + RETURNED + DISPOSED + TRANSFERRED +} + +enum LocationType { + WAREHOUSE + TECHNICIAN + SUBSCRIBER +} + // ============================================================================= // MODELS // ============================================================================= @@ -401,6 +427,8 @@ model User { createdJobOrders JobOrder[] @relation("JobOrderCreatedBy") /// Technician profiles for this user (one per tenant, enforced by @@unique([tenantId, userId])) technicianProfiles TechnicianProfile[] + /// Stock movements performed/recorded by this user + recordedMovements StockMovement[] @relation("MovementPerformedBy") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -855,3 +883,73 @@ model JobTypeRate { /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) } + +/// An InventoryItem represents a piece of ISP equipment (router, ONU, cable, etc.). +/// Serialized items have unique serial numbers and are tracked individually. +/// Batch items (consumables like cables, connectors) are tracked by type+quantity. +/// Stock levels are NEVER stored — always derived from movement history. +model InventoryItem { + id String @id @default(uuid()) + tenantId String + /// Human-readable name (e.g., "Huawei HG8145V5 ONU") + name String + /// Category type (e.g., "Router", "ONU", "Cable", "Connector") + itemType String + /// Brand/model identifier for serialized items (optional for batch) + model String? + /// Unique serial number — required for SERIALIZED, null for BATCH + serialNumber String? + trackingType ItemTrackingType + /// Purchase cost per unit (2 decimal places) + purchaseCost Decimal? @db.Decimal(10, 2) + purchaseDate DateTime? + warrantyExpiry DateTime? + /// Soft-delete: inactive items are not shown in active lists + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + movements StockMovement[] + + /// Serial numbers must be unique within a tenant (only enforced for non-null values) + @@unique([tenantId, serialNumber]) + /// RLS-ready index — always present on tenant-scoped models + @@index([tenantId]) + @@index([tenantId, itemType]) + @@index([tenantId, trackingType]) +} + +/// A StockMovement is an immutable record of inventory movement. +/// Stock levels are derived by aggregating movements — no mutable quantity columns. +/// RECEIVED movements auto-post journal entries (DR 1200 Equipment Inventory, CR 2010 AP). +model StockMovement { + id String @id @default(uuid()) + tenantId String + /// The inventory item this movement affects + inventoryItemId String + inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id]) + movementType MovementType + /// Quantity moved — always 1 for SERIALIZED items, variable for BATCH + quantity Int @default(1) + /// Condition of the item at time of movement + condition ItemCondition? + /// Source location (null for RECEIVED — items come from external supplier) + fromLocationType LocationType? + fromLocationId String? + /// Destination location (null for DISPOSED — items leave the system) + toLocationType LocationType? + toLocationId String? + notes String? + /// Journal entry created for RECEIVED movements (DR 1200, CR 2010) + journalEntryId String? + /// The user who recorded this movement + performedById String + performedBy User @relation("MovementPerformedBy", fields: [performedById], references: [id]) + /// Immutable — no updatedAt. Movements cannot be modified, only new movements added. + createdAt DateTime @default(now()) + + /// RLS-ready index — always present on tenant-scoped models + @@index([tenantId]) + @@index([inventoryItemId]) + @@index([tenantId, movementType]) +}