ca-grow-ops-manager/backend/prisma/schema.prisma
fullsizemalt c962118ba6
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix(prisma): Add inverse relation for PlantType specific plants
2026-01-01 16:35:52 -08:00

1136 lines
29 KiB
Text

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum RoleEnum {
OWNER
MANAGER
GROWER
STAFF
}
enum RoomType {
VEG
FLOWER
DRY
CURE
MOTHER
CLONE
FACILITY
}
enum TaskStatus {
PENDING
IN_PROGRESS
COMPLETED
BLOCKED
}
// Daily Walkthrough Enums
enum WalkthroughStatus {
IN_PROGRESS
COMPLETED
INCOMPLETE
}
enum TankType {
VEG
FLOWER
}
enum TankStatus {
OK
LOW
CRITICAL
}
enum HealthStatus {
GOOD
FAIR
NEEDS_ATTENTION
}
enum AccessStatus {
OK
ISSUES
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
name String?
role RoleEnum @default(STAFF) // Kept for legacy/fallback, but relying on roleId usually
roleId String?
userRole Role? @relation(fields: [roleId], references: [id])
rate Decimal? @map("hourly_rate")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
timeLogs TimeLog[]
walkthroughs DailyWalkthrough[]
touchPoints PlantTouchPoint[]
// Phase 3 Relations
weightLogs WeightLog[]
batchNotes BatchNote[]
batchPhotos BatchPhoto[]
// Phase 8: Visitor Management
visitorEscorts VisitorLog[] @relation("VisitorEscort")
visitorApprovals VisitorLog[] @relation("VisitorApprover")
// Phase 9: Messaging
announcements Announcement[] @relation("AnnouncementCreator")
announcementAcks AnnouncementAck[] @relation("AnnouncementAcker")
shiftNotes ShiftNote[] @relation("ShiftNoteCreator")
// Phase 10: Compliance
documentsCreated Document[] @relation("DocumentCreator")
documentsApproved Document[] @relation("DocumentApprover")
@@map("users")
}
model Role {
id String @id @default(uuid())
name String @unique
description String?
permissions Json // Store permissions as JSON: { users: { read: true, write: true }, ... }
isSystem Boolean @default(false) // System roles cannot be deleted
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("roles")
}
model Room {
id String @id @default(uuid())
name String
type RoomType
sqft Float?
width Float?
length Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
capacity Int? // plant count or sq ft
status RoomStatus @default(ACTIVE)
targetTemp Float?
targetHumidity Float?
targetVPD Float?
batches Batch[]
tasks Task[]
@@map("rooms")
}
model Batch {
id String @id @default(uuid())
name String // e.g., "B-2023-10-15-GG4"
strain String
startDate DateTime
harvestDate DateTime?
status String @default("ACTIVE") // ACTIVE, HARVESTED, COMPLETED
plantCount Int @default(0)
source BatchSource @default(CLONE)
stage BatchStage @default(CLONE_IN)
metrcTags String[] // array of METRC tag IDs
roomId String?
room Room? @relation(fields: [roomId], references: [id])
tasks Task[]
touchPoints PlantTouchPoint[]
ipmSchedule IPMSchedule?
// Phase 3 Relations
weights WeightLog[]
notes BatchNote[]
photos BatchPhoto[]
facilityPlants FacilityPlant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("batches")
}
model TaskTemplate {
id String @id @default(uuid())
title String
description String? // Instructions/SOP
roomType RoomType?
estimatedMinutes Int?
materials String[] // Array of material names
recurrence Json? // Cron or custom pattern
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@map("task_templates")
}
model Task {
id String @id @default(uuid())
title String
description String?
status TaskStatus @default(PENDING)
priority String @default("MEDIUM")
templateId String?
template TaskTemplate? @relation(fields: [templateId], references: [id])
assignedToId String?
assignedTo User? @relation(fields: [assignedToId], references: [id])
batchId String?
batch Batch? @relation(fields: [batchId], references: [id])
roomId String?
room Room? @relation(fields: [roomId], references: [id])
dueDate DateTime?
completedAt DateTime?
notes String?
photos String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("tasks")
}
model TimeLog {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
startTime DateTime
endTime DateTime?
activityType String? // e.g. "Trimming", "Feeding", "Cleaning"
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("time_logs")
}
// Daily Walkthrough Models
model DailyWalkthrough {
id String @id @default(uuid())
date DateTime @default(now())
completedBy String
user User @relation(fields: [completedBy], references: [id])
startTime DateTime @default(now())
endTime DateTime?
status WalkthroughStatus @default(IN_PROGRESS)
reservoirChecks ReservoirCheck[]
irrigationChecks IrrigationCheck[]
plantHealthChecks PlantHealthCheck[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("daily_walkthroughs")
}
model WalkthroughSettings {
id String @id @default("default")
// Photo Requirements
reservoirPhotos PhotoRequirement @default(OPTIONAL)
irrigationPhotos PhotoRequirement @default(OPTIONAL)
plantHealthPhotos PhotoRequirement @default(REQUIRED)
// Enabled Sections
enableReservoirs Boolean @default(true)
enableIrrigation Boolean @default(true)
enablePlantHealth Boolean @default(true)
updatedAt DateTime @updatedAt
@@map("walkthrough_settings")
}
enum PhotoRequirement {
REQUIRED
OPTIONAL
WEEKLY
ON_DEMAND
}
model ReservoirCheck {
id String @id @default(uuid())
walkthroughId String
walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade)
tankName String
tankType TankType
levelPercent Int
status TankStatus
photoUrl String?
notes String?
createdAt DateTime @default(now())
@@map("reservoir_checks")
}
model IrrigationCheck {
id String @id @default(uuid())
walkthroughId String
walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade)
zoneName String // "Veg Upstairs", "Veg Downstairs", "Flower Upstairs", "Flower Downstairs"
drippersTotal Int
drippersWorking Int
drippersFailed String? // JSON array of failed dripper IDs
waterFlow Boolean
nutrientsMixed Boolean
scheduleActive Boolean
photoUrl String? // Photo of working system
issues String?
createdAt DateTime @default(now())
@@map("irrigation_checks")
}
model PlantHealthCheck {
id String @id @default(uuid())
walkthroughId String
walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade)
zoneName String
healthStatus HealthStatus
pestsObserved Boolean
pestType String?
waterAccess AccessStatus
foodAccess AccessStatus
flaggedForAttention Boolean @default(false)
issuePhotoUrl String?
referencePhotoUrl String? // Photo of healthy system
notes String?
createdAt DateTime @default(now())
@@map("plant_health_checks")
}
// Supply/Shopping List models
model SupplyItem {
id String @id @default(uuid())
name String
category SupplyCategory
quantity Int @default(0)
minThreshold Int @default(0)
unit String // "each", "box", "roll", "gallon", etc.
location String? // "Storage Room", "Bathroom", etc.
vendor String? // "Amazon", "Local", etc.
productUrl String? // Link to reorder
lastOrdered DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("supply_items")
}
enum SupplyCategory {
FILTER // Air filters, water filters
CLEANING // Cleaning supplies
PPE // Gloves, masks, suits
OFFICE // Paper, pens, etc.
BATHROOM // Toilet paper, soap, etc.
KITCHEN // Coffee, snacks, etc.
MAINTENANCE // Tools, parts, etc.
OTHER
}
// Plant Touch Points
model PlantTouchPoint {
id String @id @default(uuid())
type TouchType
notes String?
photoUrls String[] // Changed from single photoUrl to array
// Measurements
heightCm Float?
widthCm Float?
// IPM specific
ipmProduct String? // e.g., "Pyganic 5.0"
ipmDosage String? // e.g., "1 oz per gallon"
// Issues
issuesObserved Boolean @default(false)
issueType String?
batchId String
batch Batch @relation(fields: [batchId], references: [id])
createdBy String
user User @relation(fields: [createdBy], references: [id])
createdAt DateTime @default(now())
@@map("plant_touch_points")
}
enum TouchType {
WATER
FEED
PRUNE
TRAIN
INSPECT
IPM
TRANSPLANT
HARVEST
OTHER
}
model IPMSchedule {
id String @id @default(uuid())
batchId String @unique // One schedule per batch
batch Batch @relation(fields: [batchId], references: [id])
product String // "Pyganic 5.0"
intervalDays Int // 10
lastTreatment DateTime?
nextTreatment DateTime? // Calculated
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("ipm_schedules")
}
// --- Phase 3: Batch Lifecycle & Room Management ---
enum BatchSource {
CLONE
SEED
}
enum BatchStage {
CLONE_IN
VEGETATIVE
FLOWERING
HARVEST
DRYING
CURING
FINISHED
}
enum RoomStatus {
ACTIVE
CLEANING
MAINTENANCE
}
enum WeightType {
WET
BUCKED
FINAL_DRY
TRIM
WASTE
}
model WeightLog {
id String @id @default(uuid())
batchId String
batch Batch @relation(fields: [batchId], references: [id])
weightType WeightType
weight Float // grams
unit String @default("g")
notes String?
photos String[] // URLs
loggedBy String
user User @relation(fields: [loggedBy], references: [id])
createdAt DateTime @default(now())
@@map("weight_logs")
}
model BatchNote {
id String @id @default(uuid())
batchId String
batch Batch @relation(fields: [batchId], references: [id])
content String // markdown
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
@@map("batch_notes")
}
model BatchPhoto {
id String @id @default(uuid())
batchId String
batch Batch @relation(fields: [batchId], references: [id])
url String
caption String?
uploadedBy String
user User @relation(fields: [uploadedBy], references: [id])
createdAt DateTime @default(now())
@@map("batch_photos")
}
// ============================================
// FACILITY LAYOUT MODELS (Phase 7)
// ============================================
enum SectionType {
TABLE
RACK
TRAY
HANGER
FLOOR
}
// Plant Type Library (Rackula-inspired DeviceType pattern)
enum PlantCategory {
VEG
FLOWER
MOTHER
CLONE
SEEDLING
}
model PlantType {
id String @id @default(uuid())
slug String @unique // kebab-case identifier (e.g., "gorilla-glue-4")
name String // Display name
strain String? // Strain name
category PlantCategory // VEG, FLOWER, MOTHER, CLONE, SEEDLING
colour String // Hex color for display (e.g., "#4A90D9")
growthDays Int? // Expected days to harvest
yieldGrams Float? // Expected yield in grams
notes String?
tags String[]
customFields Json?
plants FacilityPlant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("plant_types")
}
model FacilityProperty {
id String @id @default(uuid())
name String
address String?
licenseNum String? // DCC License Number
buildings FacilityBuilding[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_properties")
}
model FacilityBuilding {
id String @id @default(uuid())
property FacilityProperty @relation(fields: [propertyId], references: [id], onDelete: Cascade)
propertyId String
name String
code String // Short code: "GROW1"
type String // CULTIVATION, PROCESSING, DRYING, STORAGE
floors FacilityFloor[]
layout Json? // Visual layout data
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_buildings")
}
model FacilityFloor {
id String @id @default(uuid())
building FacilityBuilding @relation(fields: [buildingId], references: [id], onDelete: Cascade)
buildingId String
name String // "Floor 1", "Ground Floor"
number Int // 1, 2, 3...
width Int // feet
height Int // feet
ceilingHeight Float? // feet
defaultTiers Int @default(1)
rooms FacilityRoom[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_floors")
}
model FacilityRoom {
id String @id @default(uuid())
floor FacilityFloor @relation(fields: [floorId], references: [id], onDelete: Cascade)
floorId String
name String
code String // "VEG-A"
type RoomType // VEG, FLOWER, etc.
posX Int // Position X in pixels
posY Int // Position Y in pixels
width Int // Width in pixels
height Int // Height in pixels
rotation Int @default(0)
color String? // Custom color override
sections FacilitySection[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_rooms")
}
model FacilitySection {
id String @id @default(uuid())
room FacilityRoom @relation(fields: [roomId], references: [id], onDelete: Cascade)
roomId String
name String // "Table 1"
code String // "T1"
type SectionType
posX Int // Relative to room
posY Int
width Int
height Int
rows Int // Grid rows
columns Int // Grid columns
tiers Int @default(1) // Number of vertical tiers
spacing Int @default(12) // inches between positions
positions FacilityPosition[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_sections")
}
model FacilityPosition {
id String @id @default(uuid())
section FacilitySection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
sectionId String
row Int
column Int
tier Int @default(1) // Vertical level (1 = bottom)
slot Int @default(1) // For multi-plant positions
status String @default("EMPTY") // EMPTY, OCCUPIED, RESERVED, DAMAGED
plant FacilityPlant?
@@unique([sectionId, row, column, tier, slot])
@@map("facility_positions")
}
model FacilityPlant {
id String @id @default(uuid())
tagNumber String @unique // METRC tag
batchId String?
batch Batch? @relation(fields: [batchId], references: [id])
plantTypeId String?
plantType PlantType? @relation(fields: [plantTypeId], references: [id])
position FacilityPosition @relation(fields: [positionId], references: [id])
positionId String @unique
address String // Full hierarchical address
status String @default("ACTIVE") // ACTIVE, HARVESTED, DESTROYED, TRANSFERRED
history PlantLocationHistory[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("facility_plants")
}
model PlantLocationHistory {
id String @id @default(uuid())
plant FacilityPlant @relation(fields: [plantId], references: [id], onDelete: Cascade)
plantId String
fromAddress String?
toAddress String
movedById String // User ID
reason String? // TRANSPLANT, REORGANIZE, HARVEST
movedAt DateTime @default(now())
@@map("plant_location_history")
}
// =============================================================================
// PHASE 8: VISITOR MANAGEMENT
// =============================================================================
enum VisitorType {
VISITOR
CONTRACTOR
INSPECTOR
VENDOR
DELIVERY
OTHER
}
enum VisitorStatus {
PRE_REGISTERED
CHECKED_IN
CHECKED_OUT
DENIED
REVOKED
}
model Visitor {
id String @id @default(uuid())
name String
company String?
email String?
phone String?
photoUrl String?
idType String? // Driver License, Passport, etc.
idNumber String?
type VisitorType @default(VISITOR)
purpose String
ndaAccepted Boolean @default(false)
ndaAcceptedAt DateTime?
notes String?
logs VisitorLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("visitors")
}
model VisitorLog {
id String @id @default(uuid())
visitor Visitor @relation(fields: [visitorId], references: [id], onDelete: Cascade)
visitorId String
status VisitorStatus @default(CHECKED_IN)
entryTime DateTime @default(now())
exitTime DateTime?
escort User? @relation("VisitorEscort", fields: [escortId], references: [id])
escortId String?
approvedBy User? @relation("VisitorApprover", fields: [approvedById], references: [id])
approvedById String?
badgeNumber String?
badgeExpiry DateTime?
zones String[] // AccessZone IDs visited
signature String? // Base64 or URL
temperatureF Float? // Health screening
notes String?
@@map("visitor_logs")
}
model AccessZone {
id String @id @default(uuid())
name String
code String @unique
description String?
escortRequired Boolean @default(false)
badgeRequired Boolean @default(true)
ndaRequired Boolean @default(false)
allowedTypes VisitorType[]
maxOccupancy Int?
parentZoneId String?
parentZone AccessZone? @relation("ZoneHierarchy", fields: [parentZoneId], references: [id])
childZones AccessZone[] @relation("ZoneHierarchy")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("access_zones")
}
// =============================================================================
// PHASE 9: MESSAGING & ANNOUNCEMENTS
// =============================================================================
enum AnnouncementPriority {
INFO
WARNING
CRITICAL
}
model Announcement {
id String @id @default(uuid())
title String
body String
priority AnnouncementPriority @default(INFO)
requiresAck Boolean @default(false)
targetRoles String[] // Which roles see this
expiresAt DateTime?
createdBy User @relation("AnnouncementCreator", fields: [createdById], references: [id])
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
acks AnnouncementAck[]
@@map("announcements")
}
model AnnouncementAck {
id String @id @default(uuid())
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
announcementId String
user User @relation("AnnouncementAcker", fields: [userId], references: [id])
userId String
readAt DateTime?
acknowledgedAt DateTime?
@@unique([announcementId, userId])
@@map("announcement_acks")
}
// Shift notes for handoff communication
model ShiftNote {
id String @id @default(uuid())
roomId String?
batchId String?
content String
importance String @default("NORMAL") // LOW, NORMAL, HIGH, URGENT
readBy String[] // User IDs who have read this
createdBy User @relation("ShiftNoteCreator", fields: [createdById], references: [id])
createdById String
createdAt DateTime @default(now())
expiresAt DateTime? // Auto-expire old notes
@@map("shift_notes")
}
// =============================================================================
// PHASE 10: COMPLIANCE & AUDIT TRAIL
// =============================================================================
enum AuditAction {
CREATE
UPDATE
DELETE
LOGIN
LOGOUT
ACCESS
EXPORT
APPROVE
REJECT
}
// Immutable audit log - append only
model AuditLog {
id String @id @default(uuid())
userId String?
userName String? // Denormalized for persistence
action AuditAction
entity String // Model name: Batch, Task, Plant, etc.
entityId String?
entityName String? // Denormalized description
before Json? // State before change
after Json? // State after change
changes Json? // Only the changed fields
ipAddress String?
userAgent String?
sessionId String?
metadata Json? // Additional context
timestamp DateTime @default(now())
@@index([userId])
@@index([entity, entityId])
@@index([timestamp])
@@map("audit_logs")
}
enum DocumentType {
SOP
POLICY
TRAINING
FORM
CHECKLIST
OTHER
}
enum DocumentStatus {
DRAFT
PENDING_REVIEW
APPROVED
ARCHIVED
}
model Document {
id String @id @default(uuid())
title String
type DocumentType
status DocumentStatus @default(DRAFT)
content String // Markdown or HTML
version Int @default(1)
effectiveDate DateTime?
expiresAt DateTime?
requiresAck Boolean @default(false)
targetRoles String[] // Who must read this
createdBy User @relation("DocumentCreator", fields: [createdById], references: [id])
createdById String
approvedBy User? @relation("DocumentApprover", fields: [approvedById], references: [id])
approvedById String?
approvedAt DateTime?
versions DocumentVersion[]
acks DocumentAck[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("documents")
}
model DocumentVersion {
id String @id @default(uuid())
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
documentId String
version Int
title String
content String
changedBy String // User ID
changeNotes String?
createdAt DateTime @default(now())
@@unique([documentId, version])
@@map("document_versions")
}
model DocumentAck {
id String @id @default(uuid())
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
documentId String
userId String
version Int // Which version was acknowledged
acknowledgedAt DateTime @default(now())
@@unique([documentId, userId])
@@map("document_acks")
}
// =============================================================================
// PHASE 13: ADVANCED FEATURES
// =============================================================================
// ---------------------- Environmental Monitoring ----------------------
enum SensorType {
TEMPERATURE
HUMIDITY
CO2
LIGHT_PAR
LIGHT_LUX
PH
EC
VPD
SOIL_MOISTURE
AIR_FLOW
}
model Sensor {
id String @id @default(uuid())
name String
type SensorType
roomId String?
location String? // e.g., "North wall", "Canopy level"
deviceId String? // Hardware device ID
manufacturer String?
model String?
isActive Boolean @default(true)
minThreshold Float?
maxThreshold Float?
readings SensorReading[]
alerts EnvironmentAlert[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([roomId])
@@index([type])
@@map("sensors")
}
model SensorReading {
id String @id @default(uuid())
sensor Sensor @relation(fields: [sensorId], references: [id], onDelete: Cascade)
sensorId String
value Float
unit String // °F, %, ppm, μmol/m²/s, etc.
timestamp DateTime @default(now())
@@index([sensorId, timestamp])
@@map("sensor_readings")
}
enum AlertSeverity {
INFO
WARNING
CRITICAL
EMERGENCY
}
model EnvironmentAlert {
id String @id @default(uuid())
sensor Sensor? @relation(fields: [sensorId], references: [id])
sensorId String?
roomId String?
type String // TEMP_HIGH, HUMIDITY_LOW, CO2_HIGH, etc.
severity AlertSeverity
message String
value Float?
threshold Float?
acknowledgedAt DateTime?
acknowledgedBy String?
resolvedAt DateTime?
resolvedBy String?
createdAt DateTime @default(now())
@@index([roomId, createdAt])
@@index([severity])
@@map("environment_alerts")
}
// Environment targets per room/stage
model EnvironmentProfile {
id String @id @default(uuid())
name String
stage String? // CLONE, VEG, FLOWER, DRY, CURE
roomId String?
tempMinF Float?
tempMaxF Float?
humidityMin Float?
humidityMax Float?
co2Min Float?
co2Max Float?
lightHours Float?
vpdMin Float?
vpdMax Float?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("environment_profiles")
}
// ---------------------- Financial Tracking ----------------------
enum TransactionType {
EXPENSE
REVENUE
ADJUSTMENT
}
enum ExpenseCategory {
LABOR
NUTRIENTS
SUPPLIES
EQUIPMENT
UTILITIES
RENT
LICENSES
TESTING
PACKAGING
MARKETING
OTHER
}
model FinancialTransaction {
id String @id @default(uuid())
type TransactionType
category ExpenseCategory?
amount Float
description String
batchId String? // Link to batch for cost tracking
roomId String?
vendorId String?
invoiceNumber String?
date DateTime @default(now())
createdBy String
attachments String[] // URLs to receipts/invoices
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type, date])
@@index([batchId])
@@index([category])
@@map("financial_transactions")
}
// Cost aggregation per batch
model BatchCost {
id String @id @default(uuid())
batchId String @unique
laborCost Float @default(0)
nutrientCost Float @default(0)
utilityCost Float @default(0)
supplyCost Float @default(0)
otherCost Float @default(0)
totalCost Float @default(0)
plantCount Int @default(0)
costPerPlant Float?
yieldGrams Float?
costPerGram Float?
updatedAt DateTime @updatedAt
@@map("batch_costs")
}
// Revenue tracking
model Sale {
id String @id @default(uuid())
batchId String?
product String
quantity Float
unit String // grams, ounces, units
pricePerUnit Float
totalPrice Float
customerId String?
invoiceId String?
date DateTime @default(now())
notes String?
createdBy String
createdAt DateTime @default(now())
@@index([batchId])
@@index([date])
@@map("sales")
}
// ---------------------- AI/ML Predictions ----------------------
model YieldPrediction {
id String @id @default(uuid())
batchId String
predictedYield Float // grams
confidence Float // 0-1
factors Json? // Contributing factors
predictedAt DateTime @default(now())
actualYield Float? // Filled in after harvest
accuracy Float? // Calculated after actual
@@index([batchId])
@@map("yield_predictions")
}
model AnomalyDetection {
id String @id @default(uuid())
entityType String // Batch, Room, Sensor
entityId String
anomalyType String // GROWTH_SLOW, YIELD_LOW, ENV_UNSTABLE
severity String
description String
data Json?
isResolved Boolean @default(false)
detectedAt DateTime @default(now())
@@index([entityType, entityId])
@@map("anomaly_detections")
}