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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 22:47:17 +02:00
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([]);
});
});
});