Data-integrity fixes found in a systematic review of ApiCore: - Bottling booked the honey batch amount from a value read outside the transaction and wrote an absolute result back, so two concurrent bottlings (double tap, two devices) could both pass the check and one deduction was lost. Bookings now use a conditional UPDATE inside the transaction; corrections and deletions give amounts back atomically. - Outgoing stock movements could drive stock below zero; the removal is now a conditional decrement that refuses when stock is insufficient. - Weak, conspicuous and queenless colonies were treated as "not active" and could not be moved, split or merged - uniting a queenless colony is the textbook merge. Only terminal statuses lock a colony now; merging a colony into itself and dissolving twice are rejected. - Moving a queen to another colony failed on the unique currentQueenId because the old colony kept pointing at her. The old colony now releases her (and becomes QUEENLESS); a queenless colony receiving a queen becomes ACTIVE; dissolved colonies cannot receive a queen. - Completing a task twice moved completedAt and re-sent the webhook; reopening a task kept completedAt. Recurring templates created tasks without a due date and caught up missed weeks one stale task at a time. - Automation rules created a new task and warning on every trigger (every stock removal below minimum). Tasks now record their source trigger and at most one open task per rule and object exists. The varroa threshold (mites per day) is only compared with natural-mite-fall rates, not with sample-based counts in another unit. - New opt-in Idempotency-Key support for record-creating endpoints, so the offline queue can retry after a lost response without duplicates. - Permission catalog sync crashed replicas booting concurrently on a fresh database (unique violation); it now uses createMany/skipDuplicates. - Server-side validation for inspection rating scales (1-5), unique colony/harvest lists and net weight <= gross weight. - Stale e2e expectations updated (permission count, trigger list, real PNG fixture for the file-signature check). Migration is additive only (nullable Task column, new table). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1190 lines
37 KiB
Plaintext
1190 lines
37 KiB
Plaintext
// ApiCore MVP-Datenmodell – abgeleitet aus docs/lastenheft.md Abschnitt 13, 4.2-4.22, 26.
|
||
// Prinzip: Historie statt Löschen (siehe ARCHITECTURE.md §4) – fachliche Haupttabellen
|
||
// tragen `status` + `deactivatedAt` statt Hard-Delete. Beziehungen sind relationale FKs,
|
||
// keine Freitextfelder, damit die Rückverfolgbarkeitskette (Abschnitt 16, 29) abfragbar bleibt.
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
output = "../generated/prisma"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Benutzer / Auth / Audit (Abschnitt 3, 14, 15, 16)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum RoleStatus {
|
||
ACTIVE
|
||
INACTIVE
|
||
}
|
||
|
||
model Role {
|
||
id String @id @default(cuid())
|
||
key String @unique
|
||
name String
|
||
description String?
|
||
isSystem Boolean @default(false)
|
||
status RoleStatus @default(ACTIVE)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
permissions RolePermission[]
|
||
users User[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
model Permission {
|
||
id String @id @default(cuid())
|
||
key String @unique
|
||
resource String
|
||
action String
|
||
labelDe String
|
||
groupDe String
|
||
sortOrder Int
|
||
|
||
roles RolePermission[]
|
||
|
||
@@unique([resource, action])
|
||
@@index([resource])
|
||
}
|
||
|
||
model RolePermission {
|
||
roleId String
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
permissionId String
|
||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||
grantedAt DateTime @default(now())
|
||
|
||
@@id([roleId, permissionId])
|
||
@@index([permissionId])
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
passwordHash String
|
||
name String
|
||
roleId String
|
||
role Role @relation(fields: [roleId], references: [id])
|
||
isActive Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
// TOTP-Secret AES-256-GCM-verschluesselt gespeichert (siehe
|
||
// src/auth/mfa/mfa-crypto.util.ts), nie im Klartext in der DB.
|
||
mfaEnabled Boolean @default(false)
|
||
mfaSecret String?
|
||
avatarPath String?
|
||
|
||
// Gesetzt bei jedem Passwortwechsel; JwtAuthGuard verwirft Access-Tokens,
|
||
// die vor diesem Zeitpunkt ausgestellt wurden (siehe
|
||
// PermissionResolverService.isTokenValid), damit ein gestohlener Token
|
||
// nach einer Passwortaenderung nicht bis zu 15 Minuten weiter funktioniert.
|
||
passwordChangedAt DateTime?
|
||
|
||
refreshTokens RefreshToken[]
|
||
|
||
inspections Inspection[]
|
||
varroaMeasurements VarroaMeasurement[]
|
||
swarmControlChecks SwarmControlCheck[]
|
||
treatments Treatment[]
|
||
feedings Feeding[]
|
||
harvests Harvest[]
|
||
extractions Extraction[]
|
||
honeyMeasurements HoneyMeasurement[]
|
||
stockMovements StockMovement[]
|
||
assignedTasks Task[] @relation("TaskAssignee")
|
||
createdTasks Task[] @relation("TaskCreator")
|
||
assignedTaskTemplates TaskTemplate[] @relation("TaskTemplateAssignee")
|
||
createdTaskTemplates TaskTemplate[] @relation("TaskTemplateCreator")
|
||
colonyMoves ColonyApiaryHistory[]
|
||
auditLogs AuditLog[]
|
||
bottlingBatches BottlingBatch[]
|
||
notifications Notification[]
|
||
queenEvaluations QueenEvaluation[]
|
||
webhookSubscriptions WebhookSubscription[]
|
||
documents Document[]
|
||
honeyBatchRelocations HoneyBatchRelocation[]
|
||
|
||
@@index([roleId])
|
||
}
|
||
|
||
model RefreshToken {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
tokenHash String @unique
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
revokedAt DateTime?
|
||
|
||
@@index([userId])
|
||
}
|
||
|
||
enum AuditAction {
|
||
CREATE
|
||
UPDATE
|
||
DEACTIVATE
|
||
DELETE
|
||
LOGIN
|
||
LOGIN_FAILED
|
||
LOGOUT
|
||
TOKEN_REFRESHED
|
||
MFA_ENABLED
|
||
MFA_DISABLED
|
||
}
|
||
|
||
model AuditLog {
|
||
id String @id @default(cuid())
|
||
entityType String
|
||
entityId String
|
||
action AuditAction
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
timestamp DateTime @default(now())
|
||
beforeData Json?
|
||
afterData Json?
|
||
|
||
@@index([entityType, entityId])
|
||
@@index([timestamp])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Bienenstände (Abschnitt 4.2)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ApiaryStatus {
|
||
ACTIVE
|
||
INACTIVE
|
||
}
|
||
|
||
model Apiary {
|
||
id String @id @default(cuid())
|
||
name String
|
||
description String?
|
||
location String?
|
||
latitude Float?
|
||
longitude Float?
|
||
address String?
|
||
status ApiaryStatus @default(ACTIVE)
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
colonies Colony[]
|
||
colonyMoves ColonyApiaryHistory[]
|
||
harvests Harvest[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Völkerverwaltung (Abschnitt 4.3, 4.4)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ColonyType {
|
||
PRODUCTION_COLONY
|
||
NUCLEUS_COLONY
|
||
YOUNG_COLONY
|
||
CARETAKER_COLONY
|
||
MATING_NUCLEUS
|
||
RESERVE_COLONY
|
||
}
|
||
|
||
enum ColonyStatus {
|
||
ACTIVE
|
||
WEAK
|
||
CONSPICUOUS
|
||
QUEENLESS
|
||
DISSOLVED
|
||
SOLD
|
||
DECEASED
|
||
MERGED
|
||
}
|
||
|
||
// Fuenfstufige Zustandsskala, geteilt von Brut-, Futter- und Gesundheitsstatus.
|
||
enum ColonyCondition {
|
||
EXCELLENT
|
||
GOOD
|
||
AVERAGE
|
||
WEAK
|
||
CRITICAL
|
||
}
|
||
|
||
model Colony {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
hiveType String?
|
||
originText String?
|
||
colonyType ColonyType
|
||
status ColonyStatus @default(ACTIVE)
|
||
strength Int?
|
||
broodStatus ColonyCondition?
|
||
feedStatus ColonyCondition?
|
||
healthStatus ColonyCondition?
|
||
notes String?
|
||
currentQueenId String? @unique
|
||
currentQueen Queen? @relation("ColonyCurrentQueen", fields: [currentQueenId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
apiaryHistory ColonyApiaryHistory[]
|
||
queenHistory QueenColonyHistory[]
|
||
lineageAsSource ColonyLineage[] @relation("LineageFrom")
|
||
lineageAsResult ColonyLineage[] @relation("LineageTo")
|
||
inspections Inspection[]
|
||
varroaMeasurements VarroaMeasurement[]
|
||
swarmControlChecks SwarmControlCheck[]
|
||
treatments Treatment[]
|
||
feedings Feeding[]
|
||
harvests HarvestColony[]
|
||
queenEvaluations QueenEvaluation[]
|
||
|
||
@@index([apiaryId])
|
||
@@index([status])
|
||
}
|
||
|
||
// Verschiebung Volk <-> Bienenstand, historisch nachvollziehbar (Abschnitt 4.2)
|
||
model ColonyApiaryHistory {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
movedAt DateTime @default(now())
|
||
movedByUserId String?
|
||
movedByUser User? @relation(fields: [movedByUserId], references: [id])
|
||
notes String?
|
||
|
||
@@index([colonyId])
|
||
@@index([apiaryId])
|
||
}
|
||
|
||
enum LineageType {
|
||
SPLIT
|
||
MERGE
|
||
}
|
||
|
||
// Ableger bilden (SPLIT: from=Muttervolk, to=Ableger) und Völker vereinigen
|
||
// (MERGE: mehrere from-Zeilen auf denselben to=Zielvolk) – Abschnitt 4.4.
|
||
model ColonyLineage {
|
||
id String @id @default(cuid())
|
||
type LineageType
|
||
fromColonyId String
|
||
fromColony Colony @relation("LineageFrom", fields: [fromColonyId], references: [id])
|
||
toColonyId String
|
||
toColony Colony @relation("LineageTo", fields: [toColonyId], references: [id])
|
||
date DateTime
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([fromColonyId])
|
||
@@index([toColonyId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Königinnenverwaltung (Abschnitt 4.5)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum QueenStatus {
|
||
ACTIVE
|
||
RESERVE
|
||
UNMATED
|
||
MATED
|
||
LOST
|
||
REPLACED
|
||
SOLD
|
||
DECEASED
|
||
}
|
||
|
||
model Queen {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
hatchYear Int?
|
||
breed String?
|
||
origin String?
|
||
breeder String?
|
||
markingColor String?
|
||
markingNumber String?
|
||
matingType String?
|
||
matingStatus String?
|
||
inseminationData String?
|
||
motherLine String?
|
||
fatherLine String?
|
||
motherQueenId String?
|
||
status QueenStatus @default(ACTIVE)
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
motherQueen Queen? @relation("QueenMotherline", fields: [motherQueenId], references: [id])
|
||
daughterQueens Queen[] @relation("QueenMotherline")
|
||
colony Colony? @relation("ColonyCurrentQueen")
|
||
colonyHistory QueenColonyHistory[]
|
||
evaluations QueenEvaluation[]
|
||
|
||
@@index([status])
|
||
@@index([motherQueenId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Königinnenbewertung (Abschnitt 4.7) – v1.x Modul 31
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model QueenEvaluation {
|
||
id String @id @default(cuid())
|
||
queenId String
|
||
queen Queen @relation(fields: [queenId], references: [id])
|
||
colonyId String?
|
||
colony Colony? @relation(fields: [colonyId], references: [id])
|
||
date DateTime
|
||
colonyStrength Int
|
||
gentleness Int
|
||
combBuild Int
|
||
swarmingTendency Int
|
||
broodPattern Int
|
||
honeyYield Int
|
||
notes String?
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([queenId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
// Königinnenwechsel historisch nachvollziehbar (Abschnitt 4.5)
|
||
model QueenColonyHistory {
|
||
id String @id @default(cuid())
|
||
queenId String
|
||
queen Queen @relation(fields: [queenId], references: [id])
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
startDate DateTime
|
||
endDate DateTime?
|
||
notes String?
|
||
|
||
@@index([queenId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Durchsichten / digitale Stockkarte (Abschnitt 4.6, 4.7)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Inspection {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
weather String?
|
||
temperature Float?
|
||
colonyStrength Int?
|
||
broodAmount String?
|
||
broodPattern Int?
|
||
openBroodPresent Boolean?
|
||
cappedBroodPresent Boolean?
|
||
eggsPresent Boolean?
|
||
queenSeen Boolean?
|
||
queenCellsPresent Boolean?
|
||
swarmMood Int?
|
||
gentleness Int?
|
||
combBehavior Int?
|
||
feedStatus String?
|
||
honeySuperStatus String?
|
||
healthIssues String?
|
||
varroaIssues String?
|
||
measures String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
tasks Task[]
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Aufgabenmanagement (Abschnitt 4.8)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum TaskPriority {
|
||
LOW
|
||
NORMAL
|
||
HIGH
|
||
URGENT
|
||
}
|
||
|
||
enum TaskStatus {
|
||
OPEN
|
||
IN_PROGRESS
|
||
DONE
|
||
CANCELLED
|
||
}
|
||
|
||
enum TaskRelatedType {
|
||
COLONY
|
||
APIARY
|
||
QUEEN
|
||
HONEY_BATCH
|
||
STORAGE
|
||
GENERAL
|
||
}
|
||
|
||
model Task {
|
||
id String @id @default(cuid())
|
||
title String
|
||
description String?
|
||
priority TaskPriority @default(NORMAL)
|
||
dueDate DateTime?
|
||
status TaskStatus @default(OPEN)
|
||
assignedUserId String?
|
||
assignedUser User? @relation("TaskAssignee", fields: [assignedUserId], references: [id])
|
||
createdByUserId String?
|
||
createdByUser User? @relation("TaskCreator", fields: [createdByUserId], references: [id])
|
||
relatedType TaskRelatedType @default(GENERAL)
|
||
relatedId String?
|
||
sourceInspectionId String?
|
||
sourceInspection Inspection? @relation(fields: [sourceInspectionId], references: [id])
|
||
sourceTemplateId String?
|
||
sourceTemplate TaskTemplate? @relation(fields: [sourceTemplateId], references: [id])
|
||
// Set when an automation rule created the task (not a person). Keeps an
|
||
// automatic suggestion distinguishable from a planned task, and lets the
|
||
// rule see that it already left an open task for the same object instead
|
||
// of piling up a duplicate on every further trigger.
|
||
sourceAutomationTrigger AutomationTriggerType?
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
|
||
@@index([status, dueDate])
|
||
@@index([relatedType, relatedId])
|
||
@@index([sourceAutomationTrigger, relatedType, relatedId, status])
|
||
}
|
||
|
||
enum TaskTemplateRecurrenceType {
|
||
YEARLY
|
||
MONTHLY
|
||
INTERVAL_DAYS
|
||
}
|
||
|
||
// Wiederkehrende Aufgaben-Vorlagen – erzeugen automatisch eine neue Task,
|
||
// sobald nextDueDate erreicht ist (siehe TasksService.generateDueTasks).
|
||
model TaskTemplate {
|
||
id String @id @default(cuid())
|
||
title String
|
||
description String?
|
||
priority TaskPriority @default(NORMAL)
|
||
assignedUserId String?
|
||
assignedUser User? @relation("TaskTemplateAssignee", fields: [assignedUserId], references: [id])
|
||
createdByUserId String?
|
||
createdByUser User? @relation("TaskTemplateCreator", fields: [createdByUserId], references: [id])
|
||
recurrenceType TaskTemplateRecurrenceType
|
||
recurrenceMonth Int?
|
||
recurrenceDayOfMonth Int?
|
||
intervalDays Int?
|
||
nextDueDate DateTime
|
||
active Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
generatedTasks Task[]
|
||
|
||
@@index([active, nextDueDate])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Varroamanagement (Abschnitt 4.10)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum VarroaMethod {
|
||
NATURAL_MITE_FALL
|
||
POWDERED_SUGAR
|
||
WASHING_METHOD
|
||
DRONE_BROOD_CONTROL
|
||
}
|
||
|
||
model VarroaMeasurement {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
method VarroaMethod
|
||
durationMinutes Int?
|
||
miteCount Int?
|
||
calculatedValue Float?
|
||
assessment String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Schwarmkontrolle (jenseits Lastenheft: strukturiertes Protokoll für
|
||
// Weiselzellenkontrollen während der Schwarmzeit)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum SwarmMood {
|
||
NONE
|
||
LOW
|
||
HIGH
|
||
}
|
||
|
||
enum SwarmControlAction {
|
||
NONE
|
||
QUEEN_CELLS_REMOVED
|
||
ARTIFICIAL_SWARM
|
||
COLONY_DIVIDED
|
||
OTHER
|
||
}
|
||
|
||
model SwarmControlCheck {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
queenCellsFound Int?
|
||
queenCellsCapped Boolean @default(false)
|
||
swarmMood SwarmMood @default(NONE)
|
||
actionTaken SwarmControlAction @default(NONE)
|
||
notes String?
|
||
nextCheckDate DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Behandlung / Tierarzneimittel (Abschnitt 4.11)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Treatment {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
startDate DateTime?
|
||
endDate DateTime?
|
||
product String
|
||
activeIngredient String?
|
||
amount String?
|
||
method String?
|
||
reason String?
|
||
batchNumber String?
|
||
notes String?
|
||
withdrawalPeriodDays Int?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Fütterung (Abschnitt 4.12)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum FeedType {
|
||
SUGAR_WATER
|
||
FEED_SYRUP
|
||
FEED_PASTE
|
||
OWN_HONEY
|
||
OTHER
|
||
}
|
||
|
||
model Feeding {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
feedType FeedType
|
||
product String?
|
||
amount Float
|
||
unit String
|
||
batchNumber String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Ernte / Schleuderung / Honigcharge / Rückverfolgbarkeit (Abschnitt 4.13-4.17)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Harvest {
|
||
id String @id @default(cuid())
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
honeyType String?
|
||
honeySuperCount Int?
|
||
grossWeight Float?
|
||
netWeight Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
colonies HarvestColony[]
|
||
extractions ExtractionHarvest[]
|
||
|
||
@@index([apiaryId, date])
|
||
}
|
||
|
||
model HarvestColony {
|
||
id String @id @default(cuid())
|
||
harvestId String
|
||
harvest Harvest @relation(fields: [harvestId], references: [id])
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
|
||
@@unique([harvestId, colonyId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
model Extraction {
|
||
id String @id @default(cuid())
|
||
date DateTime
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
amount Float?
|
||
room String?
|
||
waterContent Float?
|
||
temperature Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
harvests ExtractionHarvest[]
|
||
honeyBatches HoneyBatch[]
|
||
|
||
@@index([date])
|
||
}
|
||
|
||
model ExtractionHarvest {
|
||
id String @id @default(cuid())
|
||
extractionId String
|
||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||
harvestId String
|
||
harvest Harvest @relation(fields: [harvestId], references: [id])
|
||
|
||
@@unique([extractionId, harvestId])
|
||
@@index([harvestId])
|
||
}
|
||
|
||
enum HoneyBatchStatus {
|
||
RAW
|
||
EXTRACTED
|
||
CLARIFIED
|
||
CREAMED
|
||
STORED
|
||
BOTTLED
|
||
SOLD
|
||
CONSUMED
|
||
}
|
||
|
||
model HoneyBatch {
|
||
id String @id @default(cuid())
|
||
batchNumber String @unique
|
||
extractionId String
|
||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||
honeyType String?
|
||
amount Float?
|
||
waterContent Float?
|
||
storageContainerId String?
|
||
storageContainer StorageContainer? @relation(fields: [storageContainerId], references: [id])
|
||
status HoneyBatchStatus @default(RAW)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
measurements HoneyMeasurement[]
|
||
bottlingBatches BottlingBatch[]
|
||
relocations HoneyBatchRelocation[]
|
||
containerAllocations HoneyBatchContainerAllocation[]
|
||
|
||
@@index([status])
|
||
@@index([extractionId])
|
||
}
|
||
|
||
// Honigmessungen (Abschnitt 4.17) – als Teil des HoneyBatch-Moduls im MVP,
|
||
// siehe ARCHITECTURE.md Annahme 1.
|
||
model HoneyMeasurement {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
waterContent Float
|
||
measuredAt DateTime
|
||
device String?
|
||
temperature Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Lagerbehälter / grundlegende Lagerverwaltung (Abschnitt 4.18, 4.21)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum StorageContainerStatus {
|
||
EMPTY
|
||
FILLED
|
||
CLEANING
|
||
OUT_OF_SERVICE
|
||
}
|
||
|
||
model StorageContainer {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
containerType String?
|
||
capacity Float?
|
||
currentAmount Float?
|
||
location String?
|
||
status StorageContainerStatus @default(EMPTY)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
honeyBatches HoneyBatch[]
|
||
relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom")
|
||
relocationsTo HoneyBatchRelocation[] @relation("RelocationTo")
|
||
batchAllocations HoneyBatchContainerAllocation[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// Umlagerungshistorie einer Honigcharge zwischen Lagerbehältern (Abschnitt 26, v1.x Modul 32)
|
||
model HoneyBatchRelocation {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
fromStorageContainerId String?
|
||
fromStorageContainer StorageContainer? @relation("RelocationFrom", fields: [fromStorageContainerId], references: [id])
|
||
toStorageContainerId String
|
||
toStorageContainer StorageContainer @relation("RelocationTo", fields: [toStorageContainerId], references: [id])
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
notes String?
|
||
relocatedAt DateTime @default(now())
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId, relocatedAt])
|
||
}
|
||
|
||
// Aufteilung einer Honigcharge auf mehrere Lagerbehälter (z.B. 125 kg auf 5
|
||
// Eimer à 25 kg) - additiv zum obigen "primären" storageContainerId/
|
||
// Umlagern-Fluss auf HoneyBatch, der weiterhin den einfachen Ein-Behälter-
|
||
// Fall abdeckt; diese Tabelle bildet zusätzlich den Fall ab, dass eine
|
||
// Charge zu groß für einen einzelnen Behälter ist. @@unique macht das
|
||
// Zuweisen einer Menge zu einem Behälter ein Upsert (erneutes Zuweisen
|
||
// überschreibt die Menge, statt einen Duplikat-Eintrag anzulegen).
|
||
model HoneyBatchContainerAllocation {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
storageContainerId String
|
||
storageContainer StorageContainer @relation(fields: [storageContainerId], references: [id])
|
||
amount Float
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([honeyBatchId, storageContainerId])
|
||
@@index([storageContainerId])
|
||
}
|
||
|
||
enum StockCategory {
|
||
HONEY
|
||
CONSUMABLE
|
||
EQUIPMENT_MATERIAL
|
||
}
|
||
|
||
model StockItem {
|
||
id String @id @default(cuid())
|
||
name String
|
||
category StockCategory
|
||
unit String
|
||
quantity Float @default(0)
|
||
minThreshold Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
movements StockMovement[]
|
||
|
||
@@index([category])
|
||
}
|
||
|
||
enum StockMovementType {
|
||
IN
|
||
OUT
|
||
TRANSFER
|
||
ADJUSTMENT
|
||
}
|
||
|
||
model StockMovement {
|
||
id String @id @default(cuid())
|
||
stockItemId String
|
||
stockItem StockItem @relation(fields: [stockItemId], references: [id])
|
||
type StockMovementType
|
||
quantity Float
|
||
date DateTime @default(now())
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
relatedType String?
|
||
relatedId String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([stockItemId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Abfüllungen und Produkte (Abschnitt 4.19, 4.20) – v1.x Modul 23
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ProductStatus {
|
||
ACTIVE
|
||
DISCONTINUED
|
||
}
|
||
|
||
model Product {
|
||
id String @id @default(cuid())
|
||
name String
|
||
description String?
|
||
sku String? @unique
|
||
packagingSize Float?
|
||
weight Float?
|
||
salesUnit String?
|
||
status ProductStatus @default(ACTIVE)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
bottlingBatches BottlingBatch[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// Etikettenvorlage mit {{Platzhalter}}-Text, wird beim Drucken mit den
|
||
// Daten einer Abfuellung befuellt (siehe LabelTemplatesService.renderLabelsPdf).
|
||
model LabelTemplate {
|
||
id String @id @default(cuid())
|
||
name String
|
||
// Array of LabelElement objects (text/placeholder/qrCode/image/line), see
|
||
// label-templates.service.ts. Drives both the in-browser canvas designer
|
||
// and the pdfkit print renderer from the same source of truth.
|
||
elements Json @default("[]")
|
||
widthMm Float @default(90)
|
||
heightMm Float @default(35)
|
||
// A4 sheet layout. columns/rows are nullable: null means "auto-fit as many
|
||
// as fit" (the original behaviour); a real label sheet product (e.g. a
|
||
// specific Avery SKU) publishes an exact columns/rows/margin/gap grid, so
|
||
// setting these lets a template's printed positions match the physical,
|
||
// pre-die-cut sheet instead of just whatever the naive fit computes.
|
||
columns Int?
|
||
rows Int?
|
||
marginTopMm Float @default(5)
|
||
marginLeftMm Float @default(5)
|
||
gapXMm Float @default(2)
|
||
gapYMm Float @default(2)
|
||
isDefault Boolean @default(false)
|
||
active Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([active])
|
||
}
|
||
|
||
model BottlingBatch {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
productId String?
|
||
product Product? @relation(fields: [productId], references: [id])
|
||
date DateTime
|
||
jarSize Float
|
||
jarCount Int
|
||
totalAmount Float
|
||
bestBeforeDate DateTime
|
||
lotNumber String @unique
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId])
|
||
@@index([productId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Inventar (Abschnitt 4.22) – v1.x Modul 24
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum EquipmentCondition {
|
||
NEW
|
||
GOOD
|
||
WORN
|
||
DEFECTIVE
|
||
}
|
||
|
||
model Equipment {
|
||
id String @id @default(cuid())
|
||
inventoryNumber String @unique
|
||
name String
|
||
manufacturer String?
|
||
purchaseDate DateTime?
|
||
condition EquipmentCondition @default(GOOD)
|
||
location String?
|
||
maintenanceDate DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([condition])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Benachrichtigungssystem (Abschnitt 24) – v1.x Modul 26
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum NotificationPriority {
|
||
INFO
|
||
HINWEIS
|
||
WARNUNG
|
||
KRITISCH
|
||
}
|
||
|
||
model Notification {
|
||
id String @id @default(cuid())
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
priority NotificationPriority
|
||
message String
|
||
relatedType String?
|
||
relatedId String?
|
||
read Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId, read])
|
||
@@index([priority])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Webhooks (Abschnitt 27) – v1.x Modul 28
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model WebhookSubscription {
|
||
id String @id @default(cuid())
|
||
url String
|
||
events String[]
|
||
secret String
|
||
active Boolean @default(true)
|
||
createdByUserId String?
|
||
createdByUser User? @relation(fields: [createdByUserId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([active])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dokumente und Bilder (Abschnitt 4.28) – v1.x Modul 29
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum DocumentEntityType {
|
||
APIARY
|
||
COLONY
|
||
QUEEN
|
||
INSPECTION
|
||
HONEY_BATCH
|
||
}
|
||
|
||
model Document {
|
||
id String @id @default(cuid())
|
||
entityType DocumentEntityType
|
||
entityId String
|
||
fileName String
|
||
mimeType String
|
||
sizeBytes Int
|
||
storagePath String
|
||
uploadedByUserId String?
|
||
uploadedByUser User? @relation(fields: [uploadedByUserId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([entityType, entityId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Automatisierungen (Abschnitt 26) – v1.x Modul 30
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Fest hinterlegte Trigger (Code, nicht nutzerdefiniert) – die Tabelle erlaubt
|
||
// nur, jeden Trigger zu aktivieren/deaktivieren und seine Schwellenwerte
|
||
// (config) sowie den Aufgabentext (actionTemplate) anzupassen.
|
||
enum AutomationTriggerType {
|
||
QUEENLESS_NO_EGGS
|
||
VARROA_THRESHOLD
|
||
FEED_STATUS_CRITICAL
|
||
SWARM_MOOD_HIGH
|
||
COLONY_HEALTH_CRITICAL
|
||
STOCK_ITEM_LOW
|
||
HONEY_BATCH_NUMBER_FORMAT
|
||
BOTTLING_LOT_NUMBER_FORMAT
|
||
}
|
||
|
||
model AutomationRule {
|
||
id String @id @default(cuid())
|
||
triggerType AutomationTriggerType @unique
|
||
active Boolean @default(true)
|
||
config Json?
|
||
actionTemplate String?
|
||
// Running counter for the number-format rules (HONEY_BATCH_NUMBER_FORMAT /
|
||
// BOTTLING_LOT_NUMBER_FORMAT) - unused by the threshold-style rules. Kept as
|
||
// a dedicated column (not inside `config`) so it can be incremented with a
|
||
// single atomic `UPDATE ... SET sequenceValue = sequenceValue + 1`,
|
||
// race-safe under concurrent batch/bottling creation.
|
||
sequenceValue Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Website-Einstellungen – Social-Media-Links auf der öffentlichen Herkunftsseite
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Singleton row (fixed id) rather than a key/value table: exactly one set of
|
||
// site-wide social links exists, and a fixed-id upsert is simpler than
|
||
// enforcing "at most one row" any other way.
|
||
model SiteSettings {
|
||
id String @id @default("singleton")
|
||
youtubeUrl String?
|
||
instagramUrl String?
|
||
tiktokUrl String?
|
||
websiteUrl String?
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Backup-Einstellungen – GUI-Override für den automatischen Backup-Zeitplan.
|
||
// Jede der drei Umgebungsvariablen (BACKUP_STORAGE_PATH/_CRON_SCHEDULE/
|
||
// _RETENTION_COUNT) hat Vorrang vor dem hier gespeicherten Wert - diese Zeile
|
||
// greift nur pro Feld, wenn die jeweilige Env-Variable nicht gesetzt ist.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model BackupSettings {
|
||
id String @id @default("singleton")
|
||
storagePath String?
|
||
cronSchedule String?
|
||
retentionCount Int?
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Kalender-Synchronisation – einseitige Übertragung (ApiCore → M365) von
|
||
// Kalendereinträgen in den Kalender einer Microsoft-365-Gruppe via Graph API.
|
||
// Zugangsdaten (Tenant/Client-ID/-Secret) sind bewusst NICHT Teil dieses
|
||
// Modells - wie bei OIDC_CLIENT_SECRET nur über Env-Variablen (Secret), nie
|
||
// über die GUI editierbar. groupId ist keine Zugangsdaten-Information und
|
||
// daher hier GUI-konfigurierbar.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum CalendarSyncSourceType {
|
||
TASK
|
||
INSPECTION
|
||
TREATMENT
|
||
FEEDING
|
||
HARVEST
|
||
QUEEN_ASSIGNMENT
|
||
}
|
||
|
||
enum CalendarSyncStatus {
|
||
PENDING
|
||
SYNCED
|
||
FAILED
|
||
SKIPPED
|
||
}
|
||
|
||
model CalendarSyncSettings {
|
||
id String @id @default("singleton")
|
||
enabled Boolean @default(false)
|
||
groupId String?
|
||
syncTask Boolean @default(true)
|
||
syncInspection Boolean @default(true)
|
||
syncTreatment Boolean @default(true)
|
||
syncFeeding Boolean @default(true)
|
||
syncHarvest Boolean @default(true)
|
||
syncQueenAssignment Boolean @default(true)
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// Outbox statt synchronem Push direkt im Request-Pfad (anders als das
|
||
// Webhooks-Modul): Graph-API-Aufrufe sind zu langsam/fehleranfällig
|
||
// (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/
|
||
// Aufgaben-Erstellung darauf warten zu lassen. Ein periodischer Worker
|
||
// verarbeitet PENDING-Zeilen und wiederholt fehlgeschlagene Versuche.
|
||
// @@unique auf [sourceType, sourceId] macht enqueue() ein Upsert: ein
|
||
// erneutes Enqueue (z. B. nach einer Aufgaben-Änderung) aktualisiert das
|
||
// bestehende Graph-Event über die gespeicherte graphEventId, statt ein
|
||
// Duplikat anzulegen.
|
||
// ---------------------------------------------------------------------------
|
||
// Idempotenz – schützt Anlege-Anfragen vor doppelter Ausführung, wenn ein
|
||
// Client dieselbe Anfrage wiederholt (Offline-Warteschlange nach verlorener
|
||
// Antwort, Netzabbruch, erneutes Absenden). Der Client schickt pro fachlichem
|
||
// Vorgang einen eindeutigen `Idempotency-Key`; eine Wiederholung bekommt die
|
||
// gespeicherte erste Antwort statt eines zweiten Datensatzes. Siehe
|
||
// src/common/idempotency/idempotency.interceptor.ts.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum IdempotencyStatus {
|
||
PENDING
|
||
COMPLETED
|
||
}
|
||
|
||
model IdempotencyRecord {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
key String
|
||
// "POST /api/v1/inspections" - a key reused for a different endpoint is
|
||
// rejected instead of replaying an unrelated response.
|
||
scope String
|
||
status IdempotencyStatus @default(PENDING)
|
||
statusCode Int?
|
||
responseBody Json?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([userId, key])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model CalendarSyncOutboxEntry {
|
||
id String @id @default(cuid())
|
||
sourceType CalendarSyncSourceType
|
||
sourceId String
|
||
status CalendarSyncStatus @default(PENDING)
|
||
graphEventId String?
|
||
attempts Int @default(0)
|
||
lastError String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([sourceType, sourceId])
|
||
@@index([status])
|
||
}
|