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"));
|
||||
Reference in new issue
Block a user