Files
ApiCore/ApiCore-Backend/src/winter-checks/winter-check-rules.spec.ts
T
Nick SzaunigandClaude Opus 5.5 fac66882fa feat: Ein-/Auswinterung und Winterverluste je Volk
- 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]>
2026-09-28 17:32:03 +02:00

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',
]);
});
});