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 <noreply@anthropic.com>
This commit is contained in:
kevin-asprec
2026-03-05 10:17:28 +08:00
parent b5f2f3946b
commit 3712600c4e

View File

@@ -142,6 +142,32 @@ enum CompensationModel {
HYBRID HYBRID
} }
enum ItemTrackingType {
SERIALIZED
BATCH
}
enum ItemCondition {
NEW
REFURBISHED
USED
DAMAGED
}
enum MovementType {
RECEIVED
ISSUED
RETURNED
DISPOSED
TRANSFERRED
}
enum LocationType {
WAREHOUSE
TECHNICIAN
SUBSCRIBER
}
// ============================================================================= // =============================================================================
// MODELS // MODELS
// ============================================================================= // =============================================================================
@@ -401,6 +427,8 @@ model User {
createdJobOrders JobOrder[] @relation("JobOrderCreatedBy") createdJobOrders JobOrder[] @relation("JobOrderCreatedBy")
/// Technician profiles for this user (one per tenant, enforced by @@unique([tenantId, userId])) /// Technician profiles for this user (one per tenant, enforced by @@unique([tenantId, userId]))
technicianProfiles TechnicianProfile[] technicianProfiles TechnicianProfile[]
/// Stock movements performed/recorded by this user
recordedMovements StockMovement[] @relation("MovementPerformedBy")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -855,3 +883,73 @@ model JobTypeRate {
/// RLS-ready index — always present on tenant-scoped models /// RLS-ready index — always present on tenant-scoped models
@@index([tenantId]) @@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])
}