feat: T4 wave 3 - notifications per user, suggestions API, varroa bands, split/queen events
- Schema III: per-user notifications (broadcast fan-out migration, category, link, requiredPermissions filter), notification preferences and settings endpoints, calendar withdrawal switch, occupiedFrames - Suggestions API: list/summary with permission visibility and paging, accept/dismiss/reopen incl. bulk, SuggestionView on all responses - Varroa traffic light by season and method, young colonies, treatment effect window, sample-based percentage; shared withdrawal-window helper on Berlin calendar days; GET /varroa-measurements/thresholds - ColonySplit and QueenAssigned domain events with calendar and webhook handlers - Outbox claims compare against now() rounded to timestamp(3); fixes the flaky e2e claims of rows written with SQL now() Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
d99a697739
commit
f468dc5f75
87 files changed
+6022
-510
No files matched your search
@@ -0,0 +1,33 @@
|
||||
-- T4 migration 8/n: every notification belongs to exactly one user. A
|
||||
-- broadcast (userId IS NULL) is copied once per active user with its read
|
||||
-- flag and creation time, then the original is deleted. After that userId is
|
||||
-- NOT NULL; dedupeKey ("<prefix>:<userId>") makes repeated writes harmless.
|
||||
-- Check afterwards: SELECT count(*) FROM "Notification" WHERE "userId" IS NULL;
|
||||
-- must be 0 (the SET NOT NULL below fails otherwise).
|
||||
|
||||
-- DropForeignKey (was ON DELETE SET NULL, impossible for a NOT NULL column)
|
||||
ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Notification" ADD COLUMN "dedupeKey" TEXT;
|
||||
|
||||
-- Fan-out of the broadcasts to ALL active users (isActive only, no role or
|
||||
-- permission filter): a broadcast was visible to every user before, so the
|
||||
-- copies keep exactly that visibility. With no active user, broadcasts are
|
||||
-- dropped. The copies get no dedupeKey and the default requiredPermissions.
|
||||
INSERT INTO "Notification" ("id", "userId", "priority", "message", "relatedType", "relatedId", "read", "createdAt")
|
||||
SELECT gen_random_uuid()::text, u."id", n."priority", n."message", n."relatedType", n."relatedId", n."read", n."createdAt"
|
||||
FROM "Notification" n
|
||||
CROSS JOIN "User" u
|
||||
WHERE n."userId" IS NULL AND u."isActive" = true;
|
||||
|
||||
DELETE FROM "Notification" WHERE "userId" IS NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Notification" ALTER COLUMN "userId" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Notification_dedupeKey_key" ON "Notification"("dedupeKey");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- T4 migration 9/n: switch for the withdrawal-period entries of the calendar
|
||||
-- sync (on by default, like the other sources).
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CalendarSyncSettings" ADD COLUMN "syncWithdrawal" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- T4 migration 10/n: Colony.occupiedFrames, the number of occupied frames
|
||||
-- (0-40, same unit and bounds as Inspection.colonyStrength). Colony.strength
|
||||
-- (scale 1-10) is left untouched, nothing is converted.
|
||||
--
|
||||
-- Check BEFORE the deploy (hits must be clarified with the owner, never cut):
|
||||
-- SELECT count(*) FROM "Inspection" WHERE "colonyStrength" > 40;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Colony" ADD COLUMN "occupiedFrames" INTEGER;
|
||||
|
||||
ALTER TABLE "Colony" ADD CONSTRAINT "Colony_occupiedFrames_range" CHECK ("occupiedFrames" BETWEEN 0 AND 40);
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
-- T4 migration 11/n (part category, link, permissions, preferences). Runs
|
||||
-- after migration 8. category of the old rows: job failures carry
|
||||
-- relatedType 'JobRun' (and rows without relatedType are system messages) ->
|
||||
-- SYSTEM, everything else -> SUGGESTION. Only then NOT NULL.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "NotificationCategory" AS ENUM ('TASK_DUE', 'DIGEST', 'SUGGESTION', 'WITHDRAWAL', 'SEASON', 'SYSTEM');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Notification" ADD COLUMN "category" "NotificationCategory",
|
||||
ADD COLUMN "link" TEXT,
|
||||
ADD COLUMN "requiredPermissions" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
UPDATE "Notification"
|
||||
SET "category" = CASE
|
||||
WHEN "relatedType" = 'JobRun' OR "relatedType" IS NULL THEN 'SYSTEM'::"NotificationCategory"
|
||||
ELSE 'SUGGESTION'::"NotificationCategory"
|
||||
END;
|
||||
|
||||
ALTER TABLE "Notification" ALTER COLUMN "category" SET NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NotificationPreference" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"taskDueEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"digestEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"digestTime" TEXT NOT NULL DEFAULT '06:30',
|
||||
"quietFrom" TEXT DEFAULT '21:00',
|
||||
"quietUntil" TEXT DEFAULT '06:30',
|
||||
"telegramEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"telegramCategories" "NotificationCategory"[] DEFAULT ARRAY['TASK_DUE', 'DIGEST']::"NotificationCategory"[],
|
||||
"telegramDetail" BOOLEAN NOT NULL DEFAULT true,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "NotificationPreference_pkey" PRIMARY KEY ("userId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NotificationPreference" ADD CONSTRAINT "NotificationPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -103,33 +103,34 @@ model User {
|
||||
|
||||
refreshTokens RefreshToken[]
|
||||
|
||||
inspections Inspection[]
|
||||
varroaMeasurements VarroaMeasurement[]
|
||||
swarmControlChecks SwarmControlCheck[]
|
||||
winterChecks WinterCheck[]
|
||||
treatments Treatment[]
|
||||
feedings Feeding[]
|
||||
harvests Harvest[]
|
||||
extractions Extraction[]
|
||||
honeyMeasurements HoneyMeasurement[]
|
||||
stockMovements StockMovement[]
|
||||
assignedTasks Task[] @relation("TaskAssignee")
|
||||
createdTasks Task[] @relation("TaskCreator")
|
||||
decidedSuggestions Suggestion[] @relation("SuggestionDecider")
|
||||
assignedTaskTemplates TaskTemplate[] @relation("TaskTemplateAssignee")
|
||||
createdTaskTemplates TaskTemplate[] @relation("TaskTemplateCreator")
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
auditLogs AuditLog[]
|
||||
bottlingBatches BottlingBatch[]
|
||||
productionBatches ProductionBatch[]
|
||||
batchMovements BatchMovement[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
notifications Notification[]
|
||||
queenEvaluations QueenEvaluation[]
|
||||
webhookSubscriptions WebhookSubscription[]
|
||||
documents Document[]
|
||||
honeyBatchRelocations HoneyBatchRelocation[]
|
||||
apiKeys ApiKey[]
|
||||
inspections Inspection[]
|
||||
varroaMeasurements VarroaMeasurement[]
|
||||
swarmControlChecks SwarmControlCheck[]
|
||||
winterChecks WinterCheck[]
|
||||
treatments Treatment[]
|
||||
feedings Feeding[]
|
||||
harvests Harvest[]
|
||||
extractions Extraction[]
|
||||
honeyMeasurements HoneyMeasurement[]
|
||||
stockMovements StockMovement[]
|
||||
assignedTasks Task[] @relation("TaskAssignee")
|
||||
createdTasks Task[] @relation("TaskCreator")
|
||||
decidedSuggestions Suggestion[] @relation("SuggestionDecider")
|
||||
assignedTaskTemplates TaskTemplate[] @relation("TaskTemplateAssignee")
|
||||
createdTaskTemplates TaskTemplate[] @relation("TaskTemplateCreator")
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
auditLogs AuditLog[]
|
||||
bottlingBatches BottlingBatch[]
|
||||
productionBatches ProductionBatch[]
|
||||
batchMovements BatchMovement[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
notifications Notification[]
|
||||
notificationPreference NotificationPreference?
|
||||
queenEvaluations QueenEvaluation[]
|
||||
webhookSubscriptions WebhookSubscription[]
|
||||
documents Document[]
|
||||
honeyBatchRelocations HoneyBatchRelocation[]
|
||||
apiKeys ApiKey[]
|
||||
|
||||
@@index([roleId])
|
||||
}
|
||||
@@ -291,6 +292,7 @@ model Colony {
|
||||
colonyType ColonyType
|
||||
status ColonyStatus @default(ACTIVE)
|
||||
strength Int?
|
||||
occupiedFrames Int?
|
||||
broodStatus ColonyCondition?
|
||||
feedStatus ColonyCondition?
|
||||
healthStatus ColonyCondition?
|
||||
@@ -1374,21 +1376,52 @@ enum NotificationPriority {
|
||||
KRITISCH
|
||||
}
|
||||
|
||||
enum NotificationCategory {
|
||||
TASK_DUE
|
||||
DIGEST
|
||||
SUGGESTION
|
||||
WITHDRAWAL
|
||||
SEASON
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
// Jede Zeile gehoert genau einem Empfaenger (T4: Fan-out mit Rechtepruefung
|
||||
// beim Schreiben). requiredPermissions = Rechte des Empfaengers plus Rechte
|
||||
// des Inhalts; dedupeKey = "<praefix>:<userId>".
|
||||
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())
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
priority NotificationPriority
|
||||
category NotificationCategory
|
||||
message String
|
||||
relatedType String?
|
||||
relatedId String?
|
||||
link String?
|
||||
requiredPermissions String[] @default([])
|
||||
dedupeKey String? @unique
|
||||
read Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId, read])
|
||||
@@index([priority])
|
||||
}
|
||||
|
||||
// Einstellungen je Nutzer; Standard beim ersten Lesen angelegt.
|
||||
model NotificationPreference {
|
||||
userId String @id
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
taskDueEnabled Boolean @default(true)
|
||||
digestEnabled Boolean @default(true)
|
||||
digestTime String @default("06:30")
|
||||
quietFrom String? @default("21:00")
|
||||
quietUntil String? @default("06:30")
|
||||
telegramEnabled Boolean @default(false)
|
||||
telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST])
|
||||
telegramDetail Boolean @default(true)
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webhooks (Abschnitt 27) – v1.x Modul 28
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1574,6 +1607,7 @@ model CalendarSyncSettings {
|
||||
syncFeeding Boolean @default(true)
|
||||
syncHarvest Boolean @default(true)
|
||||
syncQueenAssignment Boolean @default(true)
|
||||
syncWithdrawal Boolean @default(true)
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user