feat: T4b/T4c wave 8 - weather history, phenology, honey chain, Telegram base

- Weather history from Open-Meteo behind an adapter: worker loop
  phenology-weather with backoff, Retry-After, User-Agent, chunked
  writes and forecast-to-archive correction
- Phenology: growing degree sum per DWD, phase from reports, GTS and
  calendar, calibration from previous years, daily job step,
  SeasonService reads the stored state
- Honey chain: prefill for extractions and batches (proposals only),
  plausibility and withdrawal warnings, Extraction lock, chain events
  and batch.created webhook from HoneyBatchRecorded
- Telegram base: schema, NotificationDelivery outbox, hardened bot
  client, startup check without crash, no-network-outside-loops guard
- ARCHITECTURE.md: sections 11.11-11.13, assumptions 42-46, env table

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 20:00:46 +02:00
1 parent 22382d53a2
commit 1971b2d9c4
90 files changed
+8431 -155

No files matched your search

@@ -53,6 +53,7 @@ describe('WithdrawalCheckService', () => {
treatment: { findMany: jest.Mock; findUnique: jest.Mock };
colony: { findMany: jest.Mock };
honeyBatch: { findUnique: jest.Mock };
extractionHarvest: { findMany: jest.Mock };
bottlingBatch: { findMany: jest.Mock };
};
let notifications: { recordForPermission: jest.Mock };
@@ -69,6 +70,7 @@ describe('WithdrawalCheckService', () => {
findMany: jest.fn().mockResolvedValue([{ id: 'c1', code: 'B3' }]),
},
honeyBatch: { findUnique: jest.fn() },
extractionHarvest: { findMany: jest.fn() },
bottlingBatch: { findMany: jest.fn().mockResolvedValue([]) },
};
notifications = { recordForPermission: jest.fn().mockResolvedValue([]) };
@@ -269,6 +271,35 @@ describe('WithdrawalCheckService', () => {
});
});
describe('conflictsForExtraction', () => {
it('checks the harvests of the extraction', async () => {
db.extractionHarvest.findMany.mockResolvedValue([
{ harvestId: 'h1' },
{ harvestId: 'h2' },
]);
db.harvest.findMany.mockResolvedValue([
harvestOn('h1', '2026-08-10', ['c1']),
]);
db.treatment.findMany.mockResolvedValue([treatment({})]);
const conflicts = await service.conflictsForExtraction(asDb(), 'e1');
expect(db.extractionHarvest.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { extractionId: 'e1' } }),
);
expect(db.harvest.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { id: { in: ['h1', 'h2'] } } }),
);
expect(conflicts).toEqual([conflict()]);
});
it('answers nothing for an extraction without harvests', async () => {
db.extractionHarvest.findMany.mockResolvedValue([]);
expect(await service.conflictsForExtraction(asDb(), 'e1')).toEqual([]);
});
});
describe('warningsFor', () => {
it('words the warning with colony code, product and the end day', async () => {
const warnings = await service.warningsFor(asDb(), [conflict()]);