- WinterCheck per colony, winter and season (autumn/spring), every field editable; loss causes limited to the COLOSS monitoring categories. - Summary per winter and apiary: loss rate lost/wintered, marked provisional while colonies are not yet checked in spring - an unchecked colony never counts as survived. - A loss marks the colony deceased only on explicit request, in the same transaction and audited. - Permissions winter-checks:read/create/update granted by migration to beekeeper and staff roles; winter checks included in backups. - Page "Überwinterung": next action first, one button per row until the colony is wintered, German number format. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
import {
|
|
summarizeWinter,
|
|
validateWinterCheck,
|
|
WinterSummarySource,
|
|
winterYearFor,
|
|
} from './winter-check-rules';
|
|
|
|
const d = (value: string) => new Date(`${value}T12:00:00.000Z`);
|
|
|
|
describe('winterYearFor', () => {
|
|
it('assigns autumn dates to the winter starting that year', () => {
|
|
expect(winterYearFor(d('2026-09-15'))).toBe(2026);
|
|
expect(winterYearFor(d('2026-07-01'))).toBe(2026);
|
|
});
|
|
|
|
it('assigns spring dates to the winter that started the year before', () => {
|
|
expect(winterYearFor(d('2027-03-20'))).toBe(2026);
|
|
expect(winterYearFor(d('2027-06-30'))).toBe(2026);
|
|
});
|
|
});
|
|
|
|
describe('validateWinterCheck', () => {
|
|
it('accepts a plain autumn check', () => {
|
|
expect(
|
|
validateWinterCheck({ season: 'AUTUMN', outcome: null, lossCause: null }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('rejects an outcome on an autumn check', () => {
|
|
expect(
|
|
validateWinterCheck({
|
|
season: 'AUTUMN',
|
|
outcome: 'SURVIVED',
|
|
lossCause: null,
|
|
}),
|
|
).toMatch(/Einwinterung/);
|
|
});
|
|
|
|
it('requires an outcome on a spring check', () => {
|
|
expect(
|
|
validateWinterCheck({ season: 'SPRING', outcome: null, lossCause: null }),
|
|
).toMatch(/Ergebnis/);
|
|
});
|
|
|
|
it('requires a loss cause exactly when the colony was lost', () => {
|
|
expect(
|
|
validateWinterCheck({
|
|
season: 'SPRING',
|
|
outcome: 'LOST',
|
|
lossCause: null,
|
|
}),
|
|
).toMatch(/Verlustursache/);
|
|
expect(
|
|
validateWinterCheck({
|
|
season: 'SPRING',
|
|
outcome: 'SURVIVED',
|
|
lossCause: 'DEAD_OR_EMPTY',
|
|
}),
|
|
).toMatch(/Verlustursache/);
|
|
expect(
|
|
validateWinterCheck({
|
|
season: 'SPRING',
|
|
outcome: 'LOST',
|
|
lossCause: 'QUEEN_PROBLEM',
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
function check(overrides: Partial<WinterSummarySource>): WinterSummarySource {
|
|
return {
|
|
colonyId: 'c1',
|
|
apiaryId: 'a',
|
|
apiaryName: 'Stand A',
|
|
season: 'AUTUMN',
|
|
outcome: null,
|
|
lossCause: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('summarizeWinter', () => {
|
|
it('counts outcomes against the colonies that went into winter', () => {
|
|
const summary = summarizeWinter([
|
|
check({ colonyId: 'c1' }),
|
|
check({ colonyId: 'c1', season: 'SPRING', outcome: 'SURVIVED' }),
|
|
check({ colonyId: 'c2' }),
|
|
check({
|
|
colonyId: 'c2',
|
|
season: 'SPRING',
|
|
outcome: 'LOST',
|
|
lossCause: 'DEAD_OR_EMPTY',
|
|
}),
|
|
check({ colonyId: 'c3' }),
|
|
check({ colonyId: 'c3', season: 'SPRING', outcome: 'WEAK' }),
|
|
check({ colonyId: 'c4' }),
|
|
]);
|
|
|
|
expect(summary.total).toMatchObject({
|
|
wintered: 4,
|
|
survived: 1,
|
|
weak: 1,
|
|
lost: 1,
|
|
lostByCause: { DEAD_OR_EMPTY: 1, QUEEN_PROBLEM: 0, NATURAL_DISASTER: 0 },
|
|
pendingSpring: 1,
|
|
springWithoutAutumn: 0,
|
|
lossRatePercent: 25,
|
|
isProvisional: true,
|
|
});
|
|
});
|
|
|
|
it('never counts a colony without spring check as survived', () => {
|
|
const summary = summarizeWinter([check({ colonyId: 'c1' })]);
|
|
expect(summary.total.survived).toBe(0);
|
|
expect(summary.total.pendingSpring).toBe(1);
|
|
expect(summary.total.isProvisional).toBe(true);
|
|
});
|
|
|
|
it('keeps spring checks without autumn check out of the loss rate', () => {
|
|
const summary = summarizeWinter([
|
|
check({ colonyId: 'c1' }),
|
|
check({ colonyId: 'c1', season: 'SPRING', outcome: 'SURVIVED' }),
|
|
check({
|
|
colonyId: 'c9',
|
|
season: 'SPRING',
|
|
outcome: 'LOST',
|
|
lossCause: 'NATURAL_DISASTER',
|
|
}),
|
|
]);
|
|
expect(summary.total).toMatchObject({
|
|
wintered: 1,
|
|
lost: 0,
|
|
springWithoutAutumn: 1,
|
|
lossRatePercent: 0,
|
|
isProvisional: false,
|
|
});
|
|
});
|
|
|
|
it('reports no loss rate when nothing was wintered', () => {
|
|
expect(summarizeWinter([]).total.lossRatePercent).toBeNull();
|
|
});
|
|
|
|
it('groups by apiary, sorted by name', () => {
|
|
const summary = summarizeWinter([
|
|
check({ colonyId: 'c1', apiaryId: 'b', apiaryName: 'Wald' }),
|
|
check({ colonyId: 'c2', apiaryId: 'a', apiaryName: 'Obstwiese' }),
|
|
]);
|
|
expect(summary.apiaries.map((apiary) => apiary.apiaryName)).toEqual([
|
|
'Obstwiese',
|
|
'Wald',
|
|
]);
|
|
});
|
|
});
|