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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 14:25:24 +02:00
1 parent d99a697739
commit f468dc5f75
87 files changed
+6022 -510

No files matched your search

@@ -17,6 +17,7 @@ import { MergeColonyDto } from './dto/merge-colony.dto';
import { isLivingColonyStatus } from './colony-status';
import { lockRowsForUpdate } from '../prisma/row-lock';
import { colonyTransitionEvents, lockColony } from './colony-events';
import { berlinToday, dayToDate } from '../common/dates';
export interface ColonyFilter {
status?: ColonyStatus;
@@ -194,9 +195,16 @@ export class ColoniesService {
dto: SplitColonyDto,
actingUserId: string,
): Promise<Colony> {
const parent = await this.findById(id);
this.assertActive(parent);
await this.findById(id);
const today = berlinToday();
const splitOn = dto.date ?? today;
if (splitOn > today) {
throw new BadRequestException('Das Datum liegt in der Zukunft.');
}
const queenSource = dto.queenSource ?? null;
const child = await this.prisma.$transaction(async (tx) => {
const parent = await lockColony(tx, id);
this.assertActive(parent);
const created = await tx.colony.create({
data: {
code: dto.code,
@@ -205,12 +213,13 @@ export class ColoniesService {
notes: dto.notes,
},
});
await tx.colonyLineage.create({
const lineage = await tx.colonyLineage.create({
data: {
type: 'SPLIT',
fromColonyId: parent.id,
toColonyId: created.id,
date: new Date(),
date: dayToDate(splitOn),
queenSource,
notes: dto.notes,
},
});
@@ -224,6 +233,18 @@ export class ColoniesService {
},
tx,
);
await this.events.record(tx, {
type: 'ColonySplit',
aggregateType: 'Colony',
aggregateId: created.id,
actorUserId: actingUserId,
payload: {
parentColonyId: parent.id,
lineageId: lineage.id,
queenSource,
splitOn,
},
});
return created;
});
return child;