feat(backend): Koeniginnenbewertung (Modul 31)

QueenEvaluation-Modell (Volksstaerke, Sanftmut, Wabensitz, Schwarm-
trieb, Brutbild, Honigertrag je Skala 1-5) als Sub-Ressource der
Queens-API: GET/POST /queens/:id/evaluations. Vorgezogen vor Modul
27 (Statistik), da dessen Koeniginnen-Auswertungen auf diesen Daten
aufbauen.
This commit is contained in:
Nick Szaunig committed 2026-08-22 22:55:36 +02:00
1 parent 0fc13952fb
commit 50268fa591
8 files changed
+245 -1

No files matched your search

@@ -0,0 +1,61 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsDateString,
IsInt,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
export class CreateQueenEvaluationDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
colonyId?: string;
@ApiProperty()
@IsDateString()
date!: string;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
colonyStrength!: number;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
gentleness!: number;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
combBuild!: number;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
swarmingTendency!: number;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
broodPattern!: number;
@ApiProperty({ minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
honeyYield!: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
notes?: string;
}
@@ -15,6 +15,7 @@ import type { AuthenticatedUser } from '../auth/types/authenticated-user';
import { CreateQueenDto } from './dto/create-queen.dto';
import { UpdateQueenDto } from './dto/update-queen.dto';
import { AssignColonyDto } from './dto/assign-colony.dto';
import { CreateQueenEvaluationDto } from './dto/create-queen-evaluation.dto';
import { QueensService } from './queens.service';
@ApiTags('queens')
@@ -63,4 +64,19 @@ export class QueensController {
) {
return this.queensService.assignColony(id, dto, actor.id);
}
@Get(':id/evaluations')
getEvaluations(@Param('id') id: string) {
return this.queensService.getEvaluations(id);
}
@Roles(Role.ADMIN, Role.BEEKEEPER)
@Post(':id/evaluations')
addEvaluation(
@Param('id') id: string,
@Body() dto: CreateQueenEvaluationDto,
@CurrentUser() actor: AuthenticatedUser,
) {
return this.queensService.addEvaluation(id, dto, actor.id);
}
}
@@ -18,6 +18,7 @@ describe('QueensService', () => {
};
colony: { findUnique: jest.Mock };
queenColonyHistory: { findMany: jest.Mock };
queenEvaluation: { findMany: jest.Mock; create: jest.Mock };
$transaction: jest.Mock;
};
let auditLog: { record: jest.Mock };
@@ -39,6 +40,7 @@ describe('QueensService', () => {
},
colony: { findUnique: jest.fn() },
queenColonyHistory: { findMany: jest.fn() },
queenEvaluation: { findMany: jest.fn(), create: jest.fn() },
$transaction: jest.fn((callback: (txArg: typeof tx) => unknown) =>
callback(tx),
),
@@ -93,4 +95,39 @@ describe('QueensService', () => {
expect(historyArg.data.queenId).toBe('queen-1');
expect(historyArg.data.colonyId).toBe('colony-1');
});
it('addEvaluation creates a queen evaluation and records an audit log entry', async () => {
prisma.queen.findUnique.mockResolvedValue(queen);
const created = { id: 'eval-1', queenId: 'queen-1', colonyStrength: 4 };
prisma.queenEvaluation.create.mockResolvedValueOnce(created);
const result = await service.addEvaluation(
'queen-1',
{
date: '2026-06-01',
colonyStrength: 4,
gentleness: 5,
combBuild: 3,
swarmingTendency: 2,
broodPattern: 4,
honeyYield: 5,
},
'user-1',
);
expect(result).toEqual(created);
expect(auditLog.record).toHaveBeenCalledWith(
expect.objectContaining({
entityType: 'QueenEvaluation',
action: 'CREATE',
}),
);
});
it('getEvaluations throws NotFoundException for an unknown queen', async () => {
prisma.queen.findUnique.mockResolvedValue(null);
await expect(service.getEvaluations('missing')).rejects.toBeInstanceOf(
NotFoundException,
);
});
});
@@ -5,6 +5,7 @@ import { Queen, QueenStatus } from '../../generated/prisma';
import { CreateQueenDto } from './dto/create-queen.dto';
import { UpdateQueenDto } from './dto/update-queen.dto';
import { AssignColonyDto } from './dto/assign-colony.dto';
import { CreateQueenEvaluationDto } from './dto/create-queen-evaluation.dto';
export interface QueenFilter {
status?: QueenStatus;
@@ -126,4 +127,43 @@ export class QueensService {
orderBy: { startDate: 'desc' },
});
}
async getEvaluations(id: string) {
await this.findById(id);
return this.prisma.queenEvaluation.findMany({
where: { queenId: id },
orderBy: { date: 'desc' },
});
}
async addEvaluation(
id: string,
dto: CreateQueenEvaluationDto,
actingUserId: string,
) {
await this.findById(id);
const evaluation = await this.prisma.queenEvaluation.create({
data: {
queenId: id,
colonyId: dto.colonyId,
date: new Date(dto.date),
colonyStrength: dto.colonyStrength,
gentleness: dto.gentleness,
combBuild: dto.combBuild,
swarmingTendency: dto.swarmingTendency,
broodPattern: dto.broodPattern,
honeyYield: dto.honeyYield,
notes: dto.notes,
userId: actingUserId,
},
});
await this.auditLog.record({
entityType: 'QueenEvaluation',
entityId: evaluation.id,
action: 'CREATE',
userId: actingUserId,
afterData: evaluation,
});
return evaluation;
}
}