feat: T4 wave 5 - colony end cleanup, withdrawal chain, next steps, inspection takeover UI
- Colony end releases the queen in the same transaction; open suggestions become obsolete and a CLEANUP suggestion lists open tasks and the queen; apply-cleanup validates evidence and blocks reactivated colonies - Withdrawal chain: plausibility warnings on harvest and bottling, conflict notifications also when a treatment is entered or corrected after the harvest (naming bottled lots), withdrawal spans in the calendar and M365 sync, Berlin-day end events - Next 3 steps per colony and the "Heute" endpoint with ranking and an explicit merge table by intent - Inspection-triggered rule suggestions follow the colony's latest inspection (resolve when the finding is gone, refresh when it stays) - Frontend: colony card takeover in the inspection form, feed status as condition, occupied frames 0-40; legacy offline entries keep syncing - Notifications and preferences are deleted with their user Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
02eb16924e
commit
ef33e5549b
122 files changed
+10707
-504
No files matched your search
@@ -644,6 +644,54 @@ describe('daily evaluation rules', () => {
|
||||
...unknown[],
|
||||
];
|
||||
expect(strings.join('?')).toContain('"withdrawalPeriodDays" IS NOT NULL');
|
||||
expect(values).toEqual(['2026-06-03', '2026-06-10']);
|
||||
// One day of margin on both sides: the SQL end day is a plain UTC date,
|
||||
// the event day is the Berlin day of the withdrawal-window helper.
|
||||
expect(values).toEqual(['2026-06-02', '2026-06-11']);
|
||||
});
|
||||
|
||||
it('bounds the withdrawal query by the Berlin day of the run, not by the instant it is given', async () => {
|
||||
const { db, queryRaw } = fakeDb({});
|
||||
const rule = DAILY_EVALUATION_RULES.find(
|
||||
(r) => r.type === 'WithdrawalEnded',
|
||||
)!;
|
||||
|
||||
// 2026-06-10T23:30Z is already 2026-06-11 in Berlin.
|
||||
await rule.occurrences(db, new Date('2026-06-10T23:30:00.000Z'), {
|
||||
seasons,
|
||||
day: '2026-06-11',
|
||||
});
|
||||
|
||||
const [, ...values] = queryRaw.mock.calls[0] as [
|
||||
readonly string[],
|
||||
...unknown[],
|
||||
];
|
||||
expect(values).toEqual(['2026-06-03', '2026-06-12']);
|
||||
});
|
||||
|
||||
it('takes the end day of a treatment stored with a time part from the Berlin day', async () => {
|
||||
// 2026-06-01T22:30Z is 00:30 on 2026-06-02 in Berlin: one withdrawal day
|
||||
// later the period ends on 2026-06-03, the first day of the window. The
|
||||
// plain UTC date of the SQL would say 2026-06-02.
|
||||
const { db } = fakeDb({
|
||||
treatments: [
|
||||
{
|
||||
id: 'treatment-1',
|
||||
colonyId: 'colony-1',
|
||||
date: new Date('2026-06-01T22:30:00.000Z'),
|
||||
endDate: null,
|
||||
withdrawalPeriodDays: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const events = await eventsOn(db, '2026-06-10');
|
||||
|
||||
expect(events).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'WithdrawalEnded',
|
||||
payload: { colonyId: 'colony-1', endedOn: '2026-06-03' },
|
||||
dedupeKey: 'WithdrawalEnded:treatment-1:2026-06-03',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -123,15 +123,19 @@ interface TreatmentRow {
|
||||
/** Treatment: the day after which honey may be harvested again. */
|
||||
const withdrawalEnded: DailyRule<'WithdrawalEnded'> = {
|
||||
type: 'WithdrawalEnded',
|
||||
async occurrences(db, today) {
|
||||
async occurrences(db, _today, { day: runDay }) {
|
||||
// Prisma cannot compare column arithmetic; the days are compared as
|
||||
// dates, so the session time zone plays no part.
|
||||
// dates, so the session time zone plays no part. The SQL day is a plain
|
||||
// UTC date, the event day is the Berlin day of the withdrawal-window
|
||||
// helper: they can differ by a day for a treatment stored with a time
|
||||
// part, so the query takes one day more on each side and the helper's
|
||||
// day decides (catchUpEvents keeps the window).
|
||||
const treatments = await db.$queryRaw<TreatmentRow[]>`
|
||||
SELECT "id", "colonyId", "date", "endDate", "withdrawalPeriodDays"
|
||||
FROM "Treatment"
|
||||
WHERE "withdrawalPeriodDays" IS NOT NULL
|
||||
AND (COALESCE("endDate", "date") + "withdrawalPeriodDays" * interval '1 day')::date
|
||||
BETWEEN ${utcDay(dayAfter(today, -CATCH_UP_DAYS))}::date AND ${utcDay(today)}::date`;
|
||||
BETWEEN ${addDays(runDay, -CATCH_UP_DAYS - 1)}::date AND ${addDays(runDay, 1)}::date`;
|
||||
return treatments.flatMap((treatment) => {
|
||||
const day = withdrawalEndDay(treatment);
|
||||
if (day === null) {
|
||||
|
||||
Reference in new issue
Block a user