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
+146
@@ -0,0 +1,146 @@
|
||||
-- T6 migration 1/5: veterinary medicine catalogue, packs and the pack ledger.
|
||||
-- A treatment may point at a catalogue entry and at the pack it was taken
|
||||
-- from; the pack ledger is booked, never edited. Rows of the catalogue and
|
||||
-- the packs are never deleted while something references them (RESTRICT).
|
||||
-- CHECKs: a pack holds more than nothing; a booking moves something; only a
|
||||
-- TREATMENT booking belongs to a treatment.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MedicineUnit" AS ENUM ('ML', 'G', 'PIECE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VeterinaryMedicineMovementType" AS ENUM ('TREATMENT', 'CORRECTION', 'DISPOSAL');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "DocumentEntityType" ADD VALUE 'MEDICINE_LOT';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Treatment" ADD COLUMN "doseQuantity" DOUBLE PRECISION,
|
||||
ADD COLUMN "medicineId" TEXT,
|
||||
ADD COLUMN "medicineLotId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VeterinaryMedicine" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"activeIngredient" TEXT,
|
||||
"authorisationNumber" TEXT,
|
||||
"prescriptionOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"withdrawalPeriodDays" INTEGER,
|
||||
"unit" "MedicineUnit" NOT NULL,
|
||||
"dosePerColony" DOUBLE PRECISION,
|
||||
"doseNote" TEXT,
|
||||
"applicationMethod" TEXT,
|
||||
"minTemperatureC" DOUBLE PRECISION,
|
||||
"maxTemperatureC" DOUBLE PRECISION,
|
||||
"defaultSupplier" TEXT,
|
||||
"notes" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "VeterinaryMedicine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VeterinaryMedicineLot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"medicineId" TEXT NOT NULL,
|
||||
"batchNumber" TEXT,
|
||||
"purchasedOn" TIMESTAMP(3) NOT NULL,
|
||||
"quantity" DOUBLE PRECISION NOT NULL,
|
||||
"expiresOn" TIMESTAMP(3),
|
||||
"supplier" TEXT,
|
||||
"purchaseProof" TEXT,
|
||||
"prescribingVet" TEXT,
|
||||
"notes" TEXT,
|
||||
"userId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "VeterinaryMedicineLot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VeterinaryMedicineMovement" (
|
||||
"id" TEXT NOT NULL,
|
||||
"lotId" TEXT NOT NULL,
|
||||
"type" "VeterinaryMedicineMovementType" NOT NULL,
|
||||
"quantity" DOUBLE PRECISION NOT NULL,
|
||||
"treatmentId" TEXT,
|
||||
"reason" TEXT,
|
||||
"userId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "VeterinaryMedicineMovement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "VeterinaryMedicine_active_idx" ON "VeterinaryMedicine"("active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "VeterinaryMedicineLot_medicineId_expiresOn_idx" ON "VeterinaryMedicineLot"("medicineId", "expiresOn");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "VeterinaryMedicineMovement_lotId_idx" ON "VeterinaryMedicineMovement"("lotId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "VeterinaryMedicineMovement_treatmentId_idx" ON "VeterinaryMedicineMovement"("treatmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Treatment_medicineId_idx" ON "Treatment"("medicineId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Treatment_medicineLotId_idx" ON "Treatment"("medicineLotId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Treatment" ADD CONSTRAINT "Treatment_medicineId_fkey" FOREIGN KEY ("medicineId") REFERENCES "VeterinaryMedicine"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Treatment" ADD CONSTRAINT "Treatment_medicineLotId_fkey" FOREIGN KEY ("medicineLotId") REFERENCES "VeterinaryMedicineLot"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VeterinaryMedicineLot" ADD CONSTRAINT "VeterinaryMedicineLot_medicineId_fkey" FOREIGN KEY ("medicineId") REFERENCES "VeterinaryMedicine"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VeterinaryMedicineLot" ADD CONSTRAINT "VeterinaryMedicineLot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VeterinaryMedicineMovement" ADD CONSTRAINT "VeterinaryMedicineMovement_lotId_fkey" FOREIGN KEY ("lotId") REFERENCES "VeterinaryMedicineLot"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VeterinaryMedicineMovement" ADD CONSTRAINT "VeterinaryMedicineMovement_treatmentId_fkey" FOREIGN KEY ("treatmentId") REFERENCES "Treatment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VeterinaryMedicineMovement" ADD CONSTRAINT "VeterinaryMedicineMovement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- Constraints Prisma cannot express.
|
||||
ALTER TABLE "VeterinaryMedicineLot"
|
||||
ADD CONSTRAINT "VeterinaryMedicineLot_quantity_positive" CHECK ("quantity" > 0);
|
||||
|
||||
ALTER TABLE "VeterinaryMedicineMovement"
|
||||
ADD CONSTRAINT "VeterinaryMedicineMovement_quantity_nonzero" CHECK ("quantity" <> 0);
|
||||
|
||||
ALTER TABLE "VeterinaryMedicineMovement"
|
||||
ADD CONSTRAINT "VeterinaryMedicineMovement_treatment_matches_type"
|
||||
CHECK (("type" = 'TREATMENT') = ("treatmentId" IS NOT NULL));
|
||||
|
||||
-- Permissions: the boot-time catalog sync only grants new permissions to
|
||||
-- system roles (ADMIN), so the grants are made here (same as winter_checks).
|
||||
-- API_CLIENT gets nothing: receipts and invoice numbers are not for the shop.
|
||||
INSERT INTO "Permission" (id, key, resource, action, "labelDe", "groupDe", "sortOrder")
|
||||
VALUES
|
||||
(gen_random_uuid()::text, 'veterinary-medicines:read', 'veterinary-medicines', 'read', 'Ansehen', 'Tierarzneimittel', 3700),
|
||||
(gen_random_uuid()::text, 'veterinary-medicines:create', 'veterinary-medicines', 'create', 'Anlegen', 'Tierarzneimittel', 3701),
|
||||
(gen_random_uuid()::text, 'veterinary-medicines:update', 'veterinary-medicines', 'update', 'Bearbeiten', 'Tierarzneimittel', 3702)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE p.resource = 'veterinary-medicines'
|
||||
AND (
|
||||
r.key IN ('ADMIN', 'BEEKEEPER', 'STAFF')
|
||||
OR (r.key = 'READONLY' AND p.action = 'read')
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- T6 migration 2/5: mandatory label particulars. The food business operator
|
||||
-- (name and address) is separate from the bee keeper of the Bestandsbuch; a
|
||||
-- product carries the legal name of the food; a label template carries the
|
||||
-- minimum x-height (CHECK 0.5 to 5 mm) the print check warns about. Printing
|
||||
-- a label is audited as PRINT.
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "AuditAction" ADD VALUE 'PRINT';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN "foodBusinessAddress" TEXT,
|
||||
ADD COLUMN "foodBusinessName" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "legalName" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LabelTemplate" ADD COLUMN "minXHeightMm" DOUBLE PRECISION NOT NULL DEFAULT 1.2;
|
||||
|
||||
ALTER TABLE "LabelTemplate"
|
||||
ADD CONSTRAINT "LabelTemplate_minXHeightMm_range" CHECK ("minXHeightMm" BETWEEN 0.5 AND 5);
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
-- T6 migration 3/5: authority package and readiness. ComplianceSettings is a
|
||||
-- singleton like SiteSettings; the default net-quantity digit rules are an
|
||||
-- assumption the owner has to check (ARCHITECTURE). AuthorityNotice.openKey
|
||||
-- is a nullable unique key: at most one open draft per kind and year, any
|
||||
-- number of rows without a key. ApiaryRestriction ends on or after its start.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AuthorityNoticeKind" AS ENUM ('REGISTRATION', 'HERD_CHANGE', 'TSK_REPORT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AuthorityNoticeStatus" AS ENUM ('DRAFT', 'SENT', 'CANCELLED');
|
||||
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum. They are not used in
|
||||
-- this migration (PostgreSQL forbids it inside the same transaction).
|
||||
ALTER TYPE "DocumentEntityType" ADD VALUE 'AUTHORITY_NOTICE';
|
||||
ALTER TYPE "DocumentEntityType" ADD VALUE 'APIARY_RESTRICTION';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ComplianceSettings" (
|
||||
"id" TEXT NOT NULL DEFAULT 'singleton',
|
||||
"veterinaryOfficeName" TEXT,
|
||||
"veterinaryOfficeAddress" TEXT,
|
||||
"registrationNumber" TEXT,
|
||||
"noticeOnColonyCountChange" BOOLEAN NOT NULL DEFAULT true,
|
||||
"noticeOnApiaryChange" BOOLEAN NOT NULL DEFAULT true,
|
||||
"tskReferenceDay" TEXT NOT NULL DEFAULT '01-01',
|
||||
"tskReportDueDay" TEXT,
|
||||
"tskMemberNumber" TEXT,
|
||||
"tskExcludedColonyTypes" "ColonyType"[] DEFAULT ARRAY[]::"ColonyType"[],
|
||||
"netQuantityDigitRules" JSONB NOT NULL DEFAULT '[{"maxGrams":50,"minMm":2},{"maxGrams":200,"minMm":3},{"maxGrams":1000,"minMm":4},{"maxGrams":null,"minMm":6}]',
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ComplianceSettings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuthorityNotice" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "AuthorityNoticeKind" NOT NULL,
|
||||
"status" "AuthorityNoticeStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"year" INTEGER,
|
||||
"referenceDate" TIMESTAMP(3) NOT NULL,
|
||||
"countedAt" TIMESTAMP(3) NOT NULL,
|
||||
"snapshot" JSONB NOT NULL,
|
||||
"totalColonies" INTEGER NOT NULL,
|
||||
"sentOn" TIMESTAMP(3),
|
||||
"sentVia" TEXT,
|
||||
"reference" TEXT,
|
||||
"notes" TEXT,
|
||||
"openKey" TEXT,
|
||||
"createdByUserId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AuthorityNotice_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiaryRestriction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"apiaryId" TEXT NOT NULL,
|
||||
"disease" TEXT NOT NULL,
|
||||
"authority" TEXT,
|
||||
"reference" TEXT,
|
||||
"startsOn" TIMESTAMP(3) NOT NULL,
|
||||
"endsOn" TIMESTAMP(3),
|
||||
"notes" TEXT,
|
||||
"createdByUserId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ApiaryRestriction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ComplianceAcknowledgement" (
|
||||
"id" TEXT NOT NULL,
|
||||
"itemKey" TEXT NOT NULL,
|
||||
"subjectId" TEXT NOT NULL,
|
||||
"fingerprint" TEXT NOT NULL,
|
||||
"note" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"revokedByUserId" TEXT,
|
||||
|
||||
CONSTRAINT "ComplianceAcknowledgement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AuthorityNotice_openKey_key" ON "AuthorityNotice"("openKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuthorityNotice_kind_status_idx" ON "AuthorityNotice"("kind", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuthorityNotice_year_idx" ON "AuthorityNotice"("year");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ApiaryRestriction_apiaryId_startsOn_idx" ON "ApiaryRestriction"("apiaryId", "startsOn");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ComplianceAcknowledgement_itemKey_subjectId_idx" ON "ComplianceAcknowledgement"("itemKey", "subjectId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuthorityNotice" ADD CONSTRAINT "AuthorityNotice_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiaryRestriction" ADD CONSTRAINT "ApiaryRestriction_apiaryId_fkey" FOREIGN KEY ("apiaryId") REFERENCES "Apiary"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiaryRestriction" ADD CONSTRAINT "ApiaryRestriction_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ComplianceAcknowledgement" ADD CONSTRAINT "ComplianceAcknowledgement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Constraints Prisma cannot express.
|
||||
ALTER TABLE "ApiaryRestriction"
|
||||
ADD CONSTRAINT "ApiaryRestriction_endsOn_after_startsOn"
|
||||
CHECK ("endsOn" IS NULL OR "endsOn" >= "startsOn");
|
||||
|
||||
-- Permissions (see the winter_checks migration): read for everyone who
|
||||
-- reads the records, manage (settings, notices, restrictions, readiness
|
||||
-- sign-off) only for ADMIN and BEEKEEPER. API_CLIENT gets nothing.
|
||||
INSERT INTO "Permission" (id, key, resource, action, "labelDe", "groupDe", "sortOrder")
|
||||
VALUES
|
||||
(gen_random_uuid()::text, 'compliance:read', 'compliance', 'read', 'Ansehen', 'Prüfbereitschaft & Behörden', 3800),
|
||||
(gen_random_uuid()::text, 'compliance:manage', 'compliance', 'manage', 'Verwalten', 'Prüfbereitschaft & Behörden', 3801)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE p.resource = 'compliance'
|
||||
AND (
|
||||
r.key IN ('ADMIN', 'BEEKEEPER')
|
||||
OR (r.key IN ('STAFF', 'READONLY') AND p.action = 'read')
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- T6 migration 4/5: notification category for the authority hints ("Bestand
|
||||
-- geaendert", "TSK-Stichtag"). The value is only added here; migration 5
|
||||
-- uses it (PostgreSQL forbids using a new enum value in the same
|
||||
-- transaction that adds it).
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "NotificationCategory" ADD VALUE 'COMPLIANCE';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-- T6 migration 5/5: COMPLIANCE is a Telegram category chosen by default. New
|
||||
-- preferences start with it; every existing preference gets it appended
|
||||
-- (the app shows all categories, the list only steers Telegram). A user can
|
||||
-- still remove it in the profile: an empty list is a deliberate opt-out and
|
||||
-- stays empty (only lists with at least one entry get the new category).
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "NotificationPreference" ALTER COLUMN "telegramCategories" SET DEFAULT ARRAY['TASK_DUE', 'DIGEST', 'COMPLIANCE']::"NotificationCategory"[];
|
||||
|
||||
UPDATE "NotificationPreference"
|
||||
SET "telegramCategories" = array_append("telegramCategories", 'COMPLIANCE'::"NotificationCategory")
|
||||
WHERE cardinality("telegramCategories") > 0
|
||||
AND NOT ('COMPLIANCE'::"NotificationCategory" = ANY("telegramCategories"));
|
||||
@@ -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