feat: T6 wave 1 – compliance schema, permissions, lock order, backup
- Migrations (additive): veterinary medicines with lots and movement ledger, treatment medicine/lot/dose links, label particulars, authority notices, apiary restrictions, compliance settings and acknowledgements; notification category COMPLIANCE, added to non-empty Telegram lists. - Permissions veterinary-medicines:* and compliance:* with role grants. - LOCKABLE_TABLES: Apiary locks before Colony; T6 tables appended. - Backup exports and restores the new tables in FK order; unsealed files skip them and report dropped medicine links; an open TSK draft in the target wins. - describeNullContracts shared helper; Telegram backlog e2e no longer depends on the time of day. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7a1480138d
commit
8a31c42578
23 files changed
+2028
-84
No files matched your search
@@ -136,6 +136,12 @@ model User {
|
||||
honeyBatchRelocations HoneyBatchRelocation[]
|
||||
apiKeys ApiKey[]
|
||||
|
||||
veterinaryMedicineLots VeterinaryMedicineLot[]
|
||||
veterinaryMedicineMovements VeterinaryMedicineMovement[]
|
||||
authorityNotices AuthorityNotice[]
|
||||
apiaryRestrictions ApiaryRestriction[]
|
||||
complianceAcknowledgements ComplianceAcknowledgement[]
|
||||
|
||||
@@index([roleId])
|
||||
}
|
||||
|
||||
@@ -202,6 +208,7 @@ enum AuditAction {
|
||||
MFA_ENABLED
|
||||
MFA_DISABLED
|
||||
ACCOUNT_LOCKED
|
||||
PRINT
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
@@ -252,6 +259,7 @@ model Apiary {
|
||||
phenologyReports PhenologyReport[]
|
||||
phenologyStates ApiaryPhenology[]
|
||||
weatherDays ApiaryWeatherDay[]
|
||||
restrictions ApiaryRestriction[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
@@ -793,11 +801,11 @@ model WinterCheck {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Treatment {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
colonyId String
|
||||
colony Colony @relation(fields: [colonyId], references: [id])
|
||||
colony Colony @relation(fields: [colonyId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
date DateTime
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
@@ -813,13 +821,25 @@ model Treatment {
|
||||
// day of the treatment - kept with the treatment because the colony may
|
||||
// be moved later - and the supplier / purchase proof / prescribing vet.
|
||||
apiaryId String?
|
||||
apiary Apiary? @relation(fields: [apiaryId], references: [id])
|
||||
apiary Apiary? @relation(fields: [apiaryId], references: [id])
|
||||
supplier String?
|
||||
purchaseProof String?
|
||||
prescribingVet String?
|
||||
createdAt DateTime @default(now())
|
||||
// T6: catalogue entry, the pack the dose was taken from and the dose per
|
||||
// colony in the medicine's unit. With a pack, supplier / proof / batch
|
||||
// number / vet are read from the pack, never copied.
|
||||
medicineId String?
|
||||
medicine VeterinaryMedicine? @relation(fields: [medicineId], references: [id], onDelete: Restrict)
|
||||
medicineLotId String?
|
||||
medicineLot VeterinaryMedicineLot? @relation(fields: [medicineLotId], references: [id], onDelete: Restrict)
|
||||
doseQuantity Float?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
medicineMovements VeterinaryMedicineMovement[]
|
||||
|
||||
@@index([colonyId, date])
|
||||
@@index([medicineId])
|
||||
@@index([medicineLotId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1125,6 +1145,8 @@ model Product {
|
||||
salesUnit String?
|
||||
// Haltbarkeit in Monaten, kein Standardwert (MHD-Vorbelegung bei der Abfuellung).
|
||||
shelfLifeMonths Int?
|
||||
// Bezeichnung des Lebensmittels auf dem Etikett, z. B. "Bluetenhonig" (T6).
|
||||
legalName String?
|
||||
status ProductStatus @default(ACTIVE)
|
||||
kind ProductKind @default(BOTTLED_HONEY)
|
||||
shopCategory ShopCategory @default(HONIG)
|
||||
@@ -1163,6 +1185,8 @@ model LabelTemplate {
|
||||
// Schnittlinie - zum Anlegen am Papierschneider. Aus bei vorgestanzten
|
||||
// Etikettenbögen, die nicht geschnitten werden.
|
||||
cropMarks Boolean @default(true)
|
||||
// Mindest-x-Hoehe der Pflichtangaben in mm (T6), CHECK 0,5 bis 5.
|
||||
minXHeightMm Float @default(1.2)
|
||||
isDefault Boolean @default(false)
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -1405,6 +1429,7 @@ enum NotificationCategory {
|
||||
WITHDRAWAL
|
||||
SEASON
|
||||
SYSTEM
|
||||
COMPLIANCE
|
||||
}
|
||||
|
||||
// Jede Zeile gehoert genau einem Empfaenger (T4: Fan-out mit Rechtepruefung
|
||||
@@ -1440,7 +1465,7 @@ model NotificationPreference {
|
||||
quietFrom String? @default("21:00")
|
||||
quietUntil String? @default("06:30")
|
||||
telegramEnabled Boolean @default(false)
|
||||
telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST])
|
||||
telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST, COMPLIANCE])
|
||||
telegramDetail Boolean @default(true)
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -1547,6 +1572,9 @@ enum DocumentEntityType {
|
||||
QUEEN
|
||||
INSPECTION
|
||||
HONEY_BATCH
|
||||
MEDICINE_LOT
|
||||
AUTHORITY_NOTICE
|
||||
APIARY_RESTRICTION
|
||||
}
|
||||
|
||||
model Document {
|
||||
@@ -1637,18 +1665,22 @@ model AutomationSequence {
|
||||
// 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?
|
||||
id String @id @default("singleton")
|
||||
youtubeUrl String?
|
||||
instagramUrl String?
|
||||
tiktokUrl String?
|
||||
websiteUrl String?
|
||||
// Name and address of the bee keeper, printed on the Bestandsbuch. Never
|
||||
// part of any public page.
|
||||
holderName String?
|
||||
holderAddress String?
|
||||
holderName String?
|
||||
holderAddress String?
|
||||
// Name/firm and postal address of the food business operator printed on
|
||||
// the label (T6) - the brand, not the bee keeper of the Bestandsbuch.
|
||||
foodBusinessName String?
|
||||
foodBusinessAddress String?
|
||||
// Country of origin printed on labels and shop output.
|
||||
originCountry String @default("Deutschland")
|
||||
updatedAt DateTime @updatedAt
|
||||
originCountry String @default("Deutschland")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1966,3 +1998,187 @@ model ApiaryWeatherDay {
|
||||
|
||||
@@id([apiaryId, day])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// T6 Pruefbereitschaft (docs/superpowers/specs/2026-10-13-t6-compliance-design.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum MedicineUnit {
|
||||
ML
|
||||
G
|
||||
PIECE
|
||||
}
|
||||
|
||||
// Katalog des Imkers. Jeder Wert kommt aus Packungsbeilage oder Rezept und
|
||||
// wird vom Imker eingetragen; der Code liefert keine Arzneimitteldaten
|
||||
// (ARCHITECTURE Annahme 10).
|
||||
model VeterinaryMedicine {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
activeIngredient String?
|
||||
authorisationNumber String?
|
||||
prescriptionOnly Boolean @default(false)
|
||||
// Wartezeit Honig in Tagen, auch 0.
|
||||
withdrawalPeriodDays Int?
|
||||
// Nur aenderbar, solange keine Packung existiert.
|
||||
unit MedicineUnit
|
||||
dosePerColony Float?
|
||||
doseNote String?
|
||||
applicationMethod String?
|
||||
minTemperatureC Float?
|
||||
maxTemperatureC Float?
|
||||
defaultSupplier String?
|
||||
notes String?
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lots VeterinaryMedicineLot[]
|
||||
treatments Treatment[]
|
||||
|
||||
@@index([active])
|
||||
}
|
||||
|
||||
// Eine gekaufte Packung. Ch.-B., Lieferant, Beleg und Tierarzt stehen hier
|
||||
// einmal und gelten fuer jede Behandlung aus dieser Packung. CHECK quantity > 0.
|
||||
model VeterinaryMedicineLot {
|
||||
id String @id @default(cuid())
|
||||
// Nur aenderbar, solange keine Bewegung existiert.
|
||||
medicineId String
|
||||
medicine VeterinaryMedicine @relation(fields: [medicineId], references: [id], onDelete: Restrict)
|
||||
batchNumber String?
|
||||
purchasedOn DateTime
|
||||
quantity Float
|
||||
expiresOn DateTime?
|
||||
supplier String?
|
||||
purchaseProof String?
|
||||
prescribingVet String?
|
||||
notes String?
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
movements VeterinaryMedicineMovement[]
|
||||
treatments Treatment[]
|
||||
|
||||
@@index([medicineId, expiresOn])
|
||||
}
|
||||
|
||||
enum VeterinaryMedicineMovementType {
|
||||
TREATMENT
|
||||
CORRECTION
|
||||
DISPOSAL
|
||||
}
|
||||
|
||||
// Bewegungsbuch der Packungen: nie geaendert, nie geloescht; Fehler werden
|
||||
// gegengebucht. Rest = quantity der Packung + Summe aller quantity.
|
||||
// CHECKs: quantity <> 0; (type = 'TREATMENT') = (treatmentId IS NOT NULL).
|
||||
model VeterinaryMedicineMovement {
|
||||
id String @id @default(cuid())
|
||||
lotId String
|
||||
lot VeterinaryMedicineLot @relation(fields: [lotId], references: [id], onDelete: Restrict)
|
||||
type VeterinaryMedicineMovementType
|
||||
quantity Float
|
||||
treatmentId String?
|
||||
treatment Treatment? @relation(fields: [treatmentId], references: [id], onDelete: Restrict)
|
||||
reason String?
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([lotId])
|
||||
@@index([treatmentId])
|
||||
}
|
||||
|
||||
// Singleton wie SiteSettings. Werte mit "Annahme" sind vom Inhaber zu pruefen.
|
||||
model ComplianceSettings {
|
||||
id String @id @default("singleton")
|
||||
veterinaryOfficeName String?
|
||||
veterinaryOfficeAddress String?
|
||||
registrationNumber String?
|
||||
noticeOnColonyCountChange Boolean @default(true)
|
||||
noticeOnApiaryChange Boolean @default(true)
|
||||
// MM-TT.
|
||||
tskReferenceDay String @default("01-01")
|
||||
tskReportDueDay String?
|
||||
tskMemberNumber String?
|
||||
tskExcludedColonyTypes ColonyType[] @default([])
|
||||
// Vorgabe in der Migration (Spec 5.4), Annahme - vom Inhaber zu pruefen.
|
||||
netQuantityDigitRules Json @default("[{\"maxGrams\":50,\"minMm\":2},{\"maxGrams\":200,\"minMm\":3},{\"maxGrams\":1000,\"minMm\":4},{\"maxGrams\":null,\"minMm\":6}]")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum AuthorityNoticeKind {
|
||||
REGISTRATION
|
||||
HERD_CHANGE
|
||||
TSK_REPORT
|
||||
}
|
||||
|
||||
enum AuthorityNoticeStatus {
|
||||
DRAFT
|
||||
SENT
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model AuthorityNotice {
|
||||
id String @id @default(cuid())
|
||||
kind AuthorityNoticeKind
|
||||
status AuthorityNoticeStatus @default(DRAFT)
|
||||
year Int?
|
||||
referenceDate DateTime
|
||||
countedAt DateTime
|
||||
snapshot Json
|
||||
totalColonies Int
|
||||
sentOn DateTime?
|
||||
sentVia String?
|
||||
reference String?
|
||||
notes String?
|
||||
// Nur bei DRAFT gesetzt ("REGISTRATION", "HERD_CHANGE", "TSK_REPORT:2027"),
|
||||
// sonst null: hoechstens ein offener Entwurf je Art und Jahr. Nullbares
|
||||
// @unique statt partiellem Index, wie dedupeKey im Rueckgrat.
|
||||
openKey String? @unique
|
||||
createdByUserId String?
|
||||
createdBy User? @relation(fields: [createdByUserId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([kind, status])
|
||||
@@index([year])
|
||||
}
|
||||
|
||||
// CHECK "endsOn" IS NULL OR "endsOn" >= "startsOn".
|
||||
model ApiaryRestriction {
|
||||
id String @id @default(cuid())
|
||||
apiaryId String
|
||||
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Restrict)
|
||||
disease String
|
||||
authority String?
|
||||
reference String?
|
||||
startsOn DateTime
|
||||
endsOn DateTime?
|
||||
notes String?
|
||||
createdByUserId String?
|
||||
createdBy User? @relation(fields: [createdByUserId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([apiaryId, startsOn])
|
||||
}
|
||||
|
||||
// "Geprueft, in Ordnung" fuer einen Pruefen-Punkt. Gilt nur, solange der
|
||||
// Fingerabdruck zum aktuellen Befund passt.
|
||||
model ComplianceAcknowledgement {
|
||||
id String @id @default(cuid())
|
||||
itemKey String
|
||||
subjectId String
|
||||
fingerprint String
|
||||
note String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
revokedByUserId String?
|
||||
|
||||
@@index([itemKey, subjectId])
|
||||
}
|
||||
Reference in new issue
Block a user