feat: allow correcting honey measurements and queen evaluations
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:
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,
|
||||
|
||||
Reference in new issue
Block a user