feat: allow correcting honey measurements and queen evaluations
Backend Image / build-and-push (push) Successful in 2m3s
Frontend Image / build-and-push (push) Successful in 1m41s

Both records could be created but never changed. A water-content reading
decides whether a batch may be sold as honey at all, and queen ratings drive
breeding decisions - a slipped digit in either is exactly the kind of wrong
value that must not be left standing.

- New PATCH /honey-batches/measurements/:measurementId behind
  honey-batches:measure, and PATCH /queens/evaluations/:evaluationId behind
  queens:evaluate. Both are addressed by the child record's own id, so the
  routes take two path segments and do not collide with PATCH /:id.
- Both log before and after data, like every other correction.
- The measurement list on the honey batch page and the rating list on the queen
  page gained an edit dialog; the rating dialog reuses the existing criteria
  list rather than restating it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-19 08:15:54 +02:00
1 parent b0e05daa42
commit 42ceb33844
14 files changed
+503 -15

No files matched your search

@@ -0,0 +1,11 @@
import { PartialType } from '@nestjs/swagger';
import { CreateHoneyMeasurementDto } from './create-honey-measurement.dto';
/**
* A water-content reading typed in wrong has to be correctable: it decides
* whether a batch may be sold as honey at all, so a wrong figure left standing
* is worse than one that was never recorded.
*/
export class UpdateHoneyMeasurementDto extends PartialType(
CreateHoneyMeasurementDto,
) {}
@@ -17,6 +17,7 @@ import type { AuthenticatedUser } from '../auth/types/authenticated-user';
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
import { UpdateHoneyMeasurementDto } from './dto/update-honey-measurement.dto';
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto';
import { HoneyBatchesService } from './honey-batches.service';
@@ -117,6 +118,20 @@ export class HoneyBatchesController {
}
@RequirePermission('honey-batches:measure')
@RequirePermission('honey-batches:measure')
@Patch('measurements/:measurementId')
updateMeasurement(
@Param('measurementId') measurementId: string,
@Body() dto: UpdateHoneyMeasurementDto,
@CurrentUser() actor: AuthenticatedUser,
) {
return this.honeyBatchesService.updateMeasurement(
measurementId,
dto,
actor.id,
);
}
@Post(':id/measurements')
addMeasurement(
@Param('id') id: string,
@@ -14,7 +14,11 @@ describe('HoneyBatchesService', () => {
create: jest.Mock;
update: jest.Mock;
};
honeyMeasurement: { create: jest.Mock };
honeyMeasurement: {
create: jest.Mock;
findUnique: jest.Mock;
update: jest.Mock;
};
storageContainer: { findUnique: jest.Mock };
honeyBatchRelocation: { create: jest.Mock; findMany: jest.Mock };
honeyBatchContainerAllocation: {
@@ -60,7 +64,11 @@ describe('HoneyBatchesService', () => {
create: jest.fn(),
update: jest.fn(),
},
honeyMeasurement: { create: jest.fn() },
honeyMeasurement: {
create: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
},
storageContainer: { findUnique: jest.fn() },
honeyBatchRelocation: { create: jest.fn(), findMany: jest.fn() },
honeyBatchContainerAllocation: {
@@ -398,4 +406,41 @@ describe('HoneyBatchesService', () => {
expect(prisma.honeyBatchRelocation.create).not.toHaveBeenCalled();
});
});
it('updateMeasurement corrects a reading and logs before/after', async () => {
const before = {
id: 'measurement-1',
honeyBatchId: 'batch-1',
waterContent: 22,
};
prisma.honeyMeasurement.findUnique.mockResolvedValue(before);
prisma.honeyMeasurement.update.mockResolvedValue({
...before,
waterContent: 17.5,
});
prisma.honeyBatch.findUnique.mockResolvedValue(honeyBatch);
await service.updateMeasurement(
'measurement-1',
{ waterContent: 17.5 },
'user-1',
);
expect(auditLog.record).toHaveBeenCalledWith(
expect.objectContaining({
entityType: 'HoneyMeasurement',
entityId: 'measurement-1',
action: 'UPDATE',
beforeData: before,
}),
);
});
it('updateMeasurement throws NotFoundException for an unknown reading', async () => {
prisma.honeyMeasurement.findUnique.mockResolvedValue(null);
await expect(
service.updateMeasurement('missing', { waterContent: 17.5 }, 'user-1'),
).rejects.toBeInstanceOf(NotFoundException);
});
});
@@ -11,6 +11,7 @@ import { HoneyBatchStatus } from '../../generated/prisma';
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
import { UpdateHoneyMeasurementDto } from './dto/update-honey-measurement.dto';
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto';
@@ -122,6 +123,35 @@ export class HoneyBatchesService {
return updated;
}
async updateMeasurement(
measurementId: string,
dto: UpdateHoneyMeasurementDto,
actingUserId: string,
) {
const before = await this.prisma.honeyMeasurement.findUnique({
where: { id: measurementId },
});
if (!before) {
throw new NotFoundException('Honey measurement not found');
}
const measurement = await this.prisma.honeyMeasurement.update({
where: { id: measurementId },
data: {
...dto,
measuredAt: dto.measuredAt ? new Date(dto.measuredAt) : undefined,
},
});
await this.auditLog.record({
entityType: 'HoneyMeasurement',
entityId: measurementId,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: measurement,
});
return this.findById(before.honeyBatchId);
}
async addMeasurement(
id: string,
dto: CreateHoneyMeasurementDto,
@@ -0,0 +1,10 @@
import { PartialType } from '@nestjs/swagger';
import { CreateQueenEvaluationDto } from './create-queen-evaluation.dto';
/**
* Breeding decisions are made from these scores, so a rating entered against
* the wrong queen or with a slipped digit has to be correctable.
*/
export class UpdateQueenEvaluationDto extends PartialType(
CreateQueenEvaluationDto,
) {}
@@ -16,6 +16,7 @@ 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 { UpdateQueenEvaluationDto } from './dto/update-queen-evaluation.dto';
import { QueensService } from './queens.service';
@ApiTags('queens')
@@ -81,6 +82,16 @@ export class QueensController {
}
@RequirePermission('queens:evaluate')
@RequirePermission('queens:evaluate')
@Patch('evaluations/:evaluationId')
updateEvaluation(
@Param('evaluationId') evaluationId: string,
@Body() dto: UpdateQueenEvaluationDto,
@CurrentUser() actor: AuthenticatedUser,
) {
return this.queensService.updateEvaluation(evaluationId, dto, actor.id);
}
@Post(':id/evaluations')
addEvaluation(
@Param('id') id: string,
@@ -13,6 +13,7 @@ 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 { UpdateQueenEvaluationDto } from './dto/update-queen-evaluation.dto';
export interface QueenFilter {
status?: QueenStatus;
@@ -227,6 +228,32 @@ export class QueensService {
});
}
async updateEvaluation(
evaluationId: string,
dto: UpdateQueenEvaluationDto,
actingUserId: string,
) {
const before = await this.prisma.queenEvaluation.findUnique({
where: { id: evaluationId },
});
if (!before) {
throw new NotFoundException('Queen evaluation not found');
}
const evaluation = await this.prisma.queenEvaluation.update({
where: { id: evaluationId },
data: { ...dto, date: dto.date ? new Date(dto.date) : undefined },
});
await this.auditLog.record({
entityType: 'QueenEvaluation',
entityId: evaluationId,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: evaluation,
});
return evaluation;
}
async addEvaluation(
id: string,
dto: CreateQueenEvaluationDto,