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

+51
View File
@@ -35,6 +35,7 @@ describe('Colonies (e2e)', () => {
let apiaryBId: string;
let colonyId: string;
let childColonyId: string;
const splitColonyIds: string[] = [];
let colonyToMergeId: string;
let healthColonyId: string;
let raceColonyId: string;
@@ -83,6 +84,7 @@ describe('Colonies (e2e)', () => {
colonyToMergeId,
healthColonyId,
raceColonyId,
...splitColonyIds,
].filter(Boolean);
// Rule tasks are written without a user - their audit entry and
// TaskCreated are not covered by the userId cleanup below.
@@ -190,6 +192,55 @@ describe('Colonies (e2e)', () => {
expect((history.body as ColonyHistoryBody).lineageAsSource).toHaveLength(1);
});
it('records ColonySplit with the given day and queen source, and refuses a future day', async () => {
await request(app.getHttpServer())
.post(`/api/v1/colonies/${colonyId}/split`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({
code: `V-${Date.now()}-F`,
colonyType: 'NUCLEUS_COLONY',
date: '2999-01-01',
})
.expect(400);
await request(app.getHttpServer())
.post(`/api/v1/colonies/${colonyId}/split`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({
code: `V-${Date.now()}-T`,
colonyType: 'NUCLEUS_COLONY',
date: '2026-05-05T23:30:00+02:00',
})
.expect(400);
const response = await request(app.getHttpServer())
.post(`/api/v1/colonies/${colonyId}/split`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({
code: `V-${Date.now()}-B`,
colonyType: 'NUCLEUS_COLONY',
date: '2026-05-05',
queenSource: 'QUEEN_CELL',
})
.expect(201);
const splitId = (response.body as ColonyBody).id;
splitColonyIds.push(splitId);
const lineage = await prisma.colonyLineage.findFirstOrThrow({
where: { toColonyId: splitId },
});
expect(lineage.queenSource).toBe('QUEEN_CELL');
expect(lineage.date.toISOString().slice(0, 10)).toBe('2026-05-05');
const event = await prisma.domainEvent.findFirstOrThrow({
where: { type: 'ColonySplit', aggregateId: splitId },
});
expect(event.payload).toEqual({
parentColonyId: colonyId,
lineageId: lineage.id,
queenSource: 'QUEEN_CELL',
splitOn: '2026-05-05',
});
});
it('merges another colony into the surviving one', async () => {
const otherColony = await request(app.getHttpServer())
.post('/api/v1/colonies')