feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops
- Bottling quality: water content above the legal limit (strict >) answers 409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar size, best-before (shelfLifeMonths, month-end clamping) and water content; impossible dates give 400; water-content checks in src/common/quality. - Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue, season header on the apiary page, confirmed revoke for all apiaries. - Frontend honey chain: "Vorschlag aus …" hints with explicit take-over, prefill status note, movement ledger with withdraw/correct dialogs. - Telegram: link tokens (hashed, single use), private-chat receiver with lease and offset CAS, bounded poison-update skip, Retry-After, pending deliveries skipped on disconnect; system API for deliveries and retry; worker-only secret wiring and manifest policy tests. - ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
16cc557462
commit
fab7f7e12f
91 files changed
+8463
-104
No files matched your search
@@ -22,9 +22,11 @@ describe('HoneyBatchesService', () => {
|
||||
};
|
||||
honeyMeasurement: {
|
||||
create: jest.Mock;
|
||||
findMany: jest.Mock;
|
||||
findUnique: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
automationRule: { findUnique: jest.Mock };
|
||||
extraction: { findUnique: jest.Mock };
|
||||
storageContainer: { findUnique: jest.Mock };
|
||||
honeyBatchRelocation: { create: jest.Mock; findMany: jest.Mock };
|
||||
@@ -85,9 +87,12 @@ describe('HoneyBatchesService', () => {
|
||||
},
|
||||
honeyMeasurement: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
// No rule row: the built-in water content limits apply.
|
||||
automationRule: { findUnique: jest.fn().mockResolvedValue(null) },
|
||||
extraction: { findUnique: jest.fn() },
|
||||
storageContainer: { findUnique: jest.fn() },
|
||||
honeyBatchRelocation: { create: jest.fn(), findMany: jest.fn() },
|
||||
@@ -949,4 +954,45 @@ describe('HoneyBatchesService', () => {
|
||||
service.updateMeasurement('missing', { waterContent: 17.5 }, 'user-1'),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
describe('water content warnings on a measurement', () => {
|
||||
function measured(newest: number) {
|
||||
prisma.honeyBatch.findUnique.mockResolvedValue({
|
||||
...honeyBatch,
|
||||
honeyType: null,
|
||||
waterContent: null,
|
||||
});
|
||||
prisma.honeyMeasurement.create.mockResolvedValue({ id: 'm1' });
|
||||
prisma.honeyMeasurement.findMany.mockResolvedValue([
|
||||
{ waterContent: newest, measuredAt: new Date('2026-08-01') },
|
||||
]);
|
||||
}
|
||||
|
||||
it('addMeasurement above the legal limit answers a warning and never refuses', async () => {
|
||||
measured(21);
|
||||
|
||||
const result = await service.addMeasurement(
|
||||
'batch-1',
|
||||
{ waterContent: 21, measuredAt: '2026-08-01T10:00:00.000Z' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(result.warnings.map((w) => w.code)).toEqual([
|
||||
'WATER_CONTENT_ABOVE_LEGAL',
|
||||
]);
|
||||
expect(prisma.honeyMeasurement.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('a good measurement answers no warning', async () => {
|
||||
measured(16.5);
|
||||
|
||||
const result = await service.addMeasurement(
|
||||
'batch-1',
|
||||
{ waterContent: 16.5, measuredAt: '2026-08-01T10:00:00.000Z' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ 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 { waterContentWarningsOfBatch } from '../common/quality/water-content';
|
||||
import { honeyBatchInclude } from './honey-batch-include';
|
||||
import {
|
||||
batchWarnings,
|
||||
@@ -403,6 +404,17 @@ export class HoneyBatchesService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The honey batch as the answer to a measurement, with the water content
|
||||
* findings of its newest value (spec 7.8). Only a report: a measurement is
|
||||
* a fact and is never refused or held back for a confirmation.
|
||||
*/
|
||||
private async withWaterWarnings(honeyBatchId: string) {
|
||||
const batch = await this.findById(honeyBatchId);
|
||||
const water = await waterContentWarningsOfBatch(this.prisma, honeyBatchId);
|
||||
return withWarnings(batch, water?.warnings ?? []);
|
||||
}
|
||||
|
||||
async updateMeasurement(
|
||||
measurementId: string,
|
||||
dto: UpdateHoneyMeasurementDto,
|
||||
@@ -429,7 +441,7 @@ export class HoneyBatchesService {
|
||||
beforeData: before,
|
||||
afterData: measurement,
|
||||
});
|
||||
return this.findById(before.honeyBatchId);
|
||||
return this.withWaterWarnings(before.honeyBatchId);
|
||||
}
|
||||
|
||||
async addMeasurement(
|
||||
@@ -453,7 +465,7 @@ export class HoneyBatchesService {
|
||||
userId: actingUserId,
|
||||
afterData: measurement,
|
||||
});
|
||||
return this.findById(id);
|
||||
return this.withWaterWarnings(id);
|
||||
}
|
||||
|
||||
async relocate(id: string, dto: RelocateHoneyBatchDto, actingUserId: string) {
|
||||
|
||||
Reference in new issue
Block a user