generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Org { id String @id @default(uuid()) name String slug String @unique createdAt DateTime @default(now()) members OrgUser[] sessions Session[] machines Machine[] heartbeats MachineHeartbeat[] kpiSnapshots MachineKpiSnapshot[] events MachineEvent[] workOrders MachineWorkOrder[] settings OrgSettings? shifts OrgShift[] machineSettings MachineSettings[] settingsAudits SettingsAudit[] invites OrgInvite[] alertPolicies AlertPolicy[] alertContacts AlertContact[] alertNotifications AlertNotification[] financialProfile OrgFinancialProfile? locationFinancialOverrides LocationFinancialOverride[] machineFinancialOverrides MachineFinancialOverride[] productCostOverrides ProductCostOverride[] reasonEntries ReasonEntry[] downtimeActions DowntimeAction[] } model User { id String @id @default(uuid()) email String @unique name String? phone String? @map("phone") passwordHash String isActive Boolean @default(true) createdAt DateTime @default(now()) emailVerifiedAt DateTime? @map("email_verified_at") emailVerificationToken String? @unique @map("email_verification_token") emailVerificationExpiresAt DateTime? @map("email_verification_expires_at") orgs OrgUser[] sessions Session[] sentInvites OrgInvite[] @relation("OrgInviteInviter") alertContacts AlertContact[] alertNotifications AlertNotification[] downtimeActionsOwned DowntimeAction[] @relation("DowntimeActionOwner") downtimeActionsCreated DowntimeAction[] @relation("DowntimeActionCreator") } model OrgUser { id String @id @default(uuid()) orgId String userId String role String @default("MEMBER") // OWNER | ADMIN | MEMBER createdAt DateTime @default(now()) org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([orgId, userId]) @@index([userId]) @@index([orgId]) } model OrgInvite { id String @id @default(uuid()) orgId String @map("org_id") email String role String @default("MEMBER") // OWNER | ADMIN | MEMBER token String @unique invitedBy String? @map("invited_by") createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime @map("expires_at") acceptedAt DateTime? @map("accepted_at") revokedAt DateTime? @map("revoked_at") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) inviter User? @relation("OrgInviteInviter", fields: [invitedBy], references: [id], onDelete: SetNull) @@index([orgId]) @@index([orgId, email]) @@index([expiresAt]) @@map("org_invites") } model Session { id String @id @default(uuid()) // cookie value orgId String userId String createdAt DateTime @default(now()) lastSeenAt DateTime @default(now()) expiresAt DateTime revokedAt DateTime? ip String? userAgent String? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([orgId]) @@index([expiresAt]) } model Machine { id String @id @default(uuid()) orgId String name String apiKey String? @unique code String? location String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tsDevice DateTime @default(now()) @map("ts") tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") pairingCode String? @unique @map("pairing_code") pairingCodeExpiresAt DateTime? @map("pairing_code_expires_at") pairingCodeUsedAt DateTime? @map("pairing_code_used_at") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) heartbeats MachineHeartbeat[] kpiSnapshots MachineKpiSnapshot[] events MachineEvent[] cycles MachineCycle[] workOrders MachineWorkOrder[] settings MachineSettings? settingsAudits SettingsAudit[] alertNotifications AlertNotification[] financialOverrides MachineFinancialOverride[] reasonEntries ReasonEntry[] downtimeActions DowntimeAction[] @@unique([orgId, name]) @@index([orgId]) } model MachineHeartbeat { id String @id @default(uuid()) orgId String machineId String ts DateTime @default(now()) tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") status String message String? ip String? fwVersion String? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId, machineId, ts]) } model MachineKpiSnapshot { id String @id @default(uuid()) orgId String machineId String ts DateTime @default(now()) workOrderId String? sku String? target Int? good Int? scrap Int? cycleCount Int? goodParts Int? scrapParts Int? cavities Int? cycleTime Float? // theoretical/target actualCycle Float? // if you want (optional) availability Float? performance Float? quality Float? oee Float? trackingEnabled Boolean? productionStarted Boolean? tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId, machineId, ts]) } model MachineEvent { id String @id @default(uuid()) orgId String machineId String ts DateTime @default(now()) topic String // "anomaly-detected" eventType String // "slow-cycle" severity String // "critical" requiresAck Boolean @default(false) title String description String? tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") // store the raw data blob so we don't lose fields data Json? workOrderId String? sku String? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId, machineId, ts]) @@index([orgId, machineId, eventType, ts]) } model MachineCycle { id String @id @default(uuid()) orgId String machineId String ts DateTime @default(now()) cycleCount Int? actualCycleTime Float theoreticalCycleTime Float? workOrderId String? sku String? cavities Int? goodDelta Int? scrapDelta Int? tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") createdAt DateTime @default(now()) machine Machine @relation(fields: [machineId], references: [id]) @@index([orgId, machineId, ts]) @@index([orgId, machineId, cycleCount]) } model MachineWorkOrder { id String @id @default(uuid()) orgId String machineId String workOrderId String sku String? targetQty Int? cycleTime Float? status String @default("PENDING") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@unique([machineId, workOrderId]) @@index([orgId, machineId]) @@index([orgId, workOrderId]) @@map("machine_work_orders") } model IngestLog { id String @id @default(uuid()) orgId String? machineId String? endpoint String schemaVersion String? seq BigInt? tsDevice DateTime? tsServer DateTime @default(now()) ok Boolean status Int errorCode String? errorMsg String? body Json? ip String? userAgent String? @@index([endpoint, tsServer]) @@index([machineId, tsServer]) @@index([machineId, seq]) } model OrgSettings { orgId String @id @map("org_id") timezone String @default("UTC") shiftChangeCompMin Int @default(10) @map("shift_change_comp_min") lunchBreakMin Int @default(30) @map("lunch_break_min") stoppageMultiplier Float @default(1.5) @map("stoppage_multiplier") oeeAlertThresholdPct Float @default(90) @map("oee_alert_threshold_pct") macroStoppageMultiplier Float @default(5) @map("macro_stoppage_multiplier") performanceThresholdPct Float @default(85) @map("performance_threshold_pct") qualitySpikeDeltaPct Float @default(5) @map("quality_spike_delta_pct") alertsJson Json? @map("alerts_json") defaultsJson Json? @map("defaults_json") version Int @default(1) updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@map("org_settings") } model OrgFinancialProfile { orgId String @id @map("org_id") defaultCurrency String @default("USD") @map("default_currency") machineCostPerMin Float? @map("machine_cost_per_min") operatorCostPerMin Float? @map("operator_cost_per_min") ratedRunningKw Float? @map("rated_running_kw") idleKw Float? @map("idle_kw") kwhRate Float? @map("kwh_rate") energyMultiplier Float @default(1.0) @map("energy_multiplier") energyCostPerMin Float? @map("energy_cost_per_min") scrapCostPerUnit Float? @map("scrap_cost_per_unit") rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@map("org_financial_profiles") } model LocationFinancialOverride { id String @id @default(uuid()) orgId String @map("org_id") location String currency String? machineCostPerMin Float? @map("machine_cost_per_min") operatorCostPerMin Float? @map("operator_cost_per_min") ratedRunningKw Float? @map("rated_running_kw") idleKw Float? @map("idle_kw") kwhRate Float? @map("kwh_rate") energyMultiplier Float? @map("energy_multiplier") energyCostPerMin Float? @map("energy_cost_per_min") scrapCostPerUnit Float? @map("scrap_cost_per_unit") rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId, location]) @@index([orgId]) @@map("location_financial_overrides") } model MachineFinancialOverride { id String @id @default(uuid()) orgId String @map("org_id") machineId String @map("machine_id") currency String? machineCostPerMin Float? @map("machine_cost_per_min") operatorCostPerMin Float? @map("operator_cost_per_min") ratedRunningKw Float? @map("rated_running_kw") idleKw Float? @map("idle_kw") kwhRate Float? @map("kwh_rate") energyMultiplier Float? @map("energy_multiplier") energyCostPerMin Float? @map("energy_cost_per_min") scrapCostPerUnit Float? @map("scrap_cost_per_unit") rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@unique([orgId, machineId]) @@index([orgId]) @@map("machine_financial_overrides") } model ProductCostOverride { id String @id @default(uuid()) orgId String @map("org_id") sku String currency String? rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId, sku]) @@index([orgId]) @@map("product_cost_overrides") } model AlertPolicy { id String @id @default(uuid()) orgId String @map("org_id") policyJson Json @map("policy_json") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId]) @@index([orgId]) @@map("alert_policies") } model AlertContact { id String @id @default(uuid()) orgId String @map("org_id") userId String? @map("user_id") name String roleScope String @map("role_scope") // MEMBER | ADMIN | OWNER | CUSTOM email String? phone String? eventTypes Json? @map("event_types") // optional allowlist (array of strings) isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) notifications AlertNotification[] @@unique([orgId, userId]) @@index([orgId]) @@index([orgId, roleScope]) @@map("alert_contacts") } model AlertNotification { id String @id @default(uuid()) orgId String @map("org_id") machineId String @map("machine_id") eventId String @map("event_id") eventType String @map("event_type") ruleId String @map("rule_id") role String channel String contactId String? @map("contact_id") userId String? @map("user_id") sentAt DateTime @default(now()) @map("sent_at") status String error String? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) contact AlertContact? @relation(fields: [contactId], references: [id], onDelete: SetNull) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([orgId, machineId, sentAt]) @@index([orgId, eventId, role, channel]) @@index([userId]) @@index([contactId]) @@map("alert_notifications") } model OrgShift { id String @id @default(uuid()) orgId String @map("org_id") name String startTime String @map("start_time") endTime String @map("end_time") sortOrder Int @map("sort_order") enabled Boolean @default(true) org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId]) @@index([orgId, sortOrder]) @@map("org_shifts") } model MachineSettings { machineId String @id @map("machine_id") orgId String @map("org_id") overridesJson Json? @map("overrides_json") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId]) @@map("machine_settings") } model SettingsAudit { id String @id @default(uuid()) orgId String @map("org_id") machineId String? @map("machine_id") actorId String? @map("actor_id") source String payloadJson Json @map("payload_json") createdAt DateTime @default(now()) @map("created_at") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine? @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId, createdAt]) @@index([machineId, createdAt]) @@map("settings_audit") } model ReasonEntry { id String @id @default(uuid()) orgId String machineId String // idempotency key from Edge (rsn_) reasonId String @unique // "downtime" | "scrap" kind String // For downtime reasons episodeId String? durationSeconds Int? episodeEndTs DateTime? // For scrap reasons scrapEntryId String? scrapQty Int? scrapUnit String? // Required reason reasonCode String reasonLabel String? reasonText String? capturedAt DateTime workOrderId String? meta Json? schemaVersion Int @default(1) createdAt DateTime @default(now()) org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) @@index([orgId, machineId, capturedAt]) @@index([orgId, kind, capturedAt]) @@unique([orgId, kind, episodeId]) @@unique([orgId, kind, scrapEntryId]) } model DowntimeAction { id String @id @default(uuid()) orgId String @map("org_id") machineId String? @map("machine_id") reasonCode String? @map("reason_code") hmDay Int? @map("hm_day") hmHour Int? @map("hm_hour") title String notes String? status String @default("open") priority String @default("medium") dueDate DateTime? @map("due_date") reminderAt DateTime? @map("reminder_at") lastReminderAt DateTime? @map("last_reminder_at") reminderStage String? @map("reminder_stage") completedAt DateTime? @map("completed_at") ownerUserId String? @map("owner_user_id") createdBy String? @map("created_by") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) machine Machine? @relation(fields: [machineId], references: [id], onDelete: SetNull) ownerUser User? @relation("DowntimeActionOwner", fields: [ownerUserId], references: [id], onDelete: SetNull) creator User? @relation("DowntimeActionCreator", fields: [createdBy], references: [id], onDelete: SetNull) @@index([orgId]) @@index([orgId, machineId]) @@index([orgId, reasonCode]) @@index([orgId, hmDay, hmHour]) @@index([orgId, status, dueDate]) @@index([ownerUserId]) @@map("downtime_actions") }