feat: T4b wave 7 - schema for phenology, honey movement ledger, shelf life
- Phenology reports, apiary phenology state and weather days with permissions for system roles - Honey movement ledger: BOTTLING movements and initialAmount backfilled from existing bottlings with the same gram rounding; the migration reports counts and aborts if the ledger invariant fails - Shelf life and event catalog columns; backup export/import of the new tables, old bundles keep importing, invariant check after imports - e2e: test timeout 20 s (app start in hooks exceeded 5 s under load) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
326b3d9cdd
commit
22382d53a2
17 files changed
+1213
-39
No files matched your search
@@ -111,6 +111,7 @@ model User {
|
||||
feedings Feeding[]
|
||||
harvests Harvest[]
|
||||
extractions Extraction[]
|
||||
phenologyReports PhenologyReport[]
|
||||
honeyMeasurements HoneyMeasurement[]
|
||||
stockMovements StockMovement[]
|
||||
assignedTasks Task[] @relation("TaskAssignee")
|
||||
@@ -240,11 +241,14 @@ model Apiary {
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
phenologyReports PhenologyReport[]
|
||||
phenologyStates ApiaryPhenology[]
|
||||
weatherDays ApiaryWeatherDay[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
@@ -928,6 +932,9 @@ model HoneyBatch {
|
||||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||||
honeyType String?
|
||||
amount Float?
|
||||
// Ausgangsmenge in kg; amount = initialAmount + Summe der Buchungen / 1000.
|
||||
// null = Menge nicht erfasst, dann wird nicht gebucht.
|
||||
initialAmount Float?
|
||||
waterContent Float?
|
||||
storageContainerId String?
|
||||
storageContainer StorageContainer? @relation(fields: [storageContainerId], references: [id])
|
||||
@@ -939,6 +946,7 @@ model HoneyBatch {
|
||||
bottlingBatches BottlingBatch[]
|
||||
relocations HoneyBatchRelocation[]
|
||||
containerAllocations HoneyBatchContainerAllocation[]
|
||||
movements BatchMovement[]
|
||||
|
||||
@@index([status])
|
||||
@@index([extractionId])
|
||||
@@ -1105,18 +1113,20 @@ enum ShopCategory {
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
sku String? @unique
|
||||
packagingSize Float?
|
||||
weight Float?
|
||||
salesUnit String?
|
||||
status ProductStatus @default(ACTIVE)
|
||||
kind ProductKind @default(BOTTLED_HONEY)
|
||||
shopCategory ShopCategory @default(HONIG)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
sku String? @unique
|
||||
packagingSize Float?
|
||||
weight Float?
|
||||
salesUnit String?
|
||||
// Haltbarkeit in Monaten, kein Standardwert (MHD-Vorbelegung bei der Abfuellung).
|
||||
shelfLifeMonths Int?
|
||||
status ProductStatus @default(ACTIVE)
|
||||
kind ProductKind @default(BOTTLED_HONEY)
|
||||
shopCategory ShopCategory @default(HONIG)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
bottlingBatches BottlingBatch[]
|
||||
productionBatches ProductionBatch[]
|
||||
@@ -1174,7 +1184,8 @@ model BottlingBatch {
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
movements BatchMovement[]
|
||||
movements BatchMovement[]
|
||||
causedMovements BatchMovement[] @relation("BatchMovementSourceBottling")
|
||||
|
||||
@@index([honeyBatchId])
|
||||
@@index([productId])
|
||||
@@ -1288,28 +1299,36 @@ enum WithdrawalReason {
|
||||
// Genau eine der beiden Chargen-Spalten ist gesetzt (CHECK-Constraint in der
|
||||
// Migration).
|
||||
model BatchMovement {
|
||||
id String @id @default(cuid())
|
||||
bottlingBatchId String?
|
||||
bottlingBatch BottlingBatch? @relation(fields: [bottlingBatchId], references: [id], onDelete: Restrict)
|
||||
productionBatchId String?
|
||||
productionBatch ProductionBatch? @relation(fields: [productionBatchId], references: [id], onDelete: Restrict)
|
||||
rawMaterialLotId String?
|
||||
rawMaterialLot RawMaterialLot? @relation(fields: [rawMaterialLotId], references: [id], onDelete: Restrict)
|
||||
type BatchMovementType
|
||||
id String @id @default(cuid())
|
||||
bottlingBatchId String?
|
||||
bottlingBatch BottlingBatch? @relation(fields: [bottlingBatchId], references: [id], onDelete: Restrict)
|
||||
productionBatchId String?
|
||||
productionBatch ProductionBatch? @relation(fields: [productionBatchId], references: [id], onDelete: Restrict)
|
||||
rawMaterialLotId String?
|
||||
rawMaterialLot RawMaterialLot? @relation(fields: [rawMaterialLotId], references: [id], onDelete: Restrict)
|
||||
// Honig: Bewegungsbuch der Honigcharge, quantity dann in Gramm.
|
||||
honeyBatchId String?
|
||||
honeyBatch HoneyBatch? @relation(fields: [honeyBatchId], references: [id], onDelete: Restrict)
|
||||
// Rueckverweis auf die verursachende Abfuellung (BOTTLING).
|
||||
sourceBottlingBatchId String?
|
||||
sourceBottlingBatch BottlingBatch? @relation("BatchMovementSourceBottling", fields: [sourceBottlingBatchId], references: [id], onDelete: SetNull)
|
||||
type BatchMovementType
|
||||
// Mit Vorzeichen: minus = raus, plus = rein.
|
||||
quantity Int
|
||||
reason WithdrawalReason?
|
||||
notes String?
|
||||
shopRequestId String?
|
||||
shopRequest ShopStockRequest? @relation(fields: [shopRequestId], references: [id], onDelete: Restrict)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
apiKeyId String?
|
||||
createdAt DateTime @default(now())
|
||||
quantity Int
|
||||
reason WithdrawalReason?
|
||||
notes String?
|
||||
shopRequestId String?
|
||||
shopRequest ShopStockRequest? @relation(fields: [shopRequestId], references: [id], onDelete: Restrict)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
apiKeyId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([bottlingBatchId])
|
||||
@@index([productionBatchId])
|
||||
@@index([rawMaterialLotId])
|
||||
@@index([honeyBatchId])
|
||||
@@index([sourceBottlingBatchId])
|
||||
@@index([shopRequestId])
|
||||
// ERP-Export /bewegungen: sortiert und blättert nach (createdAt, id).
|
||||
@@index([createdAt, id])
|
||||
@@ -1789,3 +1808,85 @@ model ThrottlerHit {
|
||||
@@id([key, throttlerName])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pflanzenentwicklung und Saisonabschnitt (T4b, Spec 4.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum PhenologyPhase {
|
||||
SALWEIDE
|
||||
KIRSCHE
|
||||
APFEL
|
||||
HOLUNDER
|
||||
LINDE
|
||||
TRACHTENDE
|
||||
HERBST
|
||||
WINTER
|
||||
}
|
||||
|
||||
enum SeasonSegment {
|
||||
WINTERRUHE
|
||||
FRUEHJAHR
|
||||
SCHWARMZEIT
|
||||
SOMMER
|
||||
SPAETSOMMER
|
||||
HERBST
|
||||
}
|
||||
|
||||
enum PhaseSource {
|
||||
REPORT
|
||||
GTS
|
||||
CALENDAR
|
||||
}
|
||||
|
||||
// Eigene Meldung per Antippen; zurueckgezogen statt geloescht.
|
||||
model PhenologyReport {
|
||||
id String @id @default(cuid())
|
||||
// null = alle Staende
|
||||
apiaryId String?
|
||||
apiary Apiary? @relation(fields: [apiaryId], references: [id], onDelete: Restrict)
|
||||
phase PhenologyPhase
|
||||
date DateTime
|
||||
notes String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([date])
|
||||
@@index([apiaryId, phase, date])
|
||||
}
|
||||
|
||||
// Abgeleiteter Zustand, schreibt nur der Job/Handler (nicht im Backup).
|
||||
model ApiaryPhenology {
|
||||
apiaryId String
|
||||
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Cascade)
|
||||
year Int
|
||||
gts Float?
|
||||
gtsThroughDay DateTime?
|
||||
missingDays Int @default(0)
|
||||
estimatedPhase PhenologyPhase?
|
||||
reportedPhase PhenologyPhase?
|
||||
effectivePhase PhenologyPhase?
|
||||
phaseSource PhaseSource?
|
||||
segment SeasonSegment
|
||||
segmentSince DateTime
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([apiaryId, year])
|
||||
}
|
||||
|
||||
// Tagesmittel je Stand, schreibt nur die Worker-Schleife (nicht im Backup).
|
||||
model ApiaryWeatherDay {
|
||||
apiaryId String
|
||||
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Cascade)
|
||||
// Kalendertag Europe/Berlin
|
||||
day DateTime
|
||||
meanTempC Float?
|
||||
// 'forecast' | 'archive'
|
||||
source String
|
||||
fetchedAt DateTime @default(now())
|
||||
|
||||
@@id([apiaryId, day])
|
||||
}
|
||||
Reference in new issue
Block a user