feat(automation): suggest follow-up checks after treatments and feedings
Two new rules, "Kontrolle nach Behandlung" and "Kontrolle nach Fütterung", create a check task when a treatment or feeding is recorded. The due date counts from the measure itself (end of treatment, else its date; feeding date) plus the configured number of days, and the task names the product and date. At most one open check per colony and rule. Both rules ship switched off and without an interval: the right timing depends on the product (package leaflet) and season, so the beekeeper sets it. Every task-creating rule can now carry a "due after N days" setting in the automation page. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
888edf7e1d
commit
5cf4e87ca3
13 files changed
+318
-11
No files matched your search
@@ -0,0 +1,173 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { createTestApp } from './utils/create-test-app';
|
||||
import { login } from './utils/api';
|
||||
|
||||
interface RuleBody {
|
||||
id: string;
|
||||
triggerType: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow-up checks after a treatment or feeding: off by default (no built-in
|
||||
* intervals), due relative to the measure itself, never duplicated.
|
||||
*/
|
||||
describe('Follow-up controls (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let prisma: PrismaService;
|
||||
const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
||||
let token: string;
|
||||
let apiaryId: string;
|
||||
|
||||
const server = () => app.getHttpServer();
|
||||
const auth = () => ({ Authorization: `Bearer ${token}` });
|
||||
|
||||
async function rule(triggerType: string): Promise<RuleBody> {
|
||||
const response = await request(server())
|
||||
.get('/api/v1/automation-rules')
|
||||
.set(auth())
|
||||
.expect(200);
|
||||
const found = (response.body as RuleBody[]).find(
|
||||
(r) => r.triggerType === triggerType,
|
||||
);
|
||||
if (!found) throw new Error(`rule ${triggerType} missing`);
|
||||
return found;
|
||||
}
|
||||
|
||||
async function newColony(code: string) {
|
||||
return prisma.colony.create({
|
||||
data: {
|
||||
code: `${code}-${suffix}`,
|
||||
apiaryId,
|
||||
colonyType: 'PRODUCTION_COLONY',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function treat(colonyId: string, body: Record<string, unknown> = {}) {
|
||||
return request(server())
|
||||
.post('/api/v1/treatments')
|
||||
.set(auth())
|
||||
.send({
|
||||
colonyId,
|
||||
date: '2026-08-01T00:00:00.000Z',
|
||||
product: 'Testmittel',
|
||||
...body,
|
||||
})
|
||||
.expect(201);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
prisma = app.get(PrismaService);
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email: `followup-${suffix}@example.com`,
|
||||
passwordHash: await bcrypt.hash('SuperSecret123!', 12),
|
||||
name: 'Follow-up',
|
||||
role: { connect: { key: 'ADMIN' } },
|
||||
},
|
||||
});
|
||||
token = (
|
||||
await login(app, `followup-${suffix}@example.com`, 'SuperSecret123!')
|
||||
).accessToken;
|
||||
apiaryId = (
|
||||
await prisma.apiary.create({ data: { name: `Follow-up ${suffix}` } })
|
||||
).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Leave the shared rules as other suites expect them: off.
|
||||
await prisma.automationRule.updateMany({
|
||||
where: {
|
||||
triggerType: { in: ['TREATMENT_FOLLOW_UP', 'FEEDING_FOLLOW_UP'] },
|
||||
},
|
||||
data: { active: false, config: {} },
|
||||
});
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('ships both follow-up rules switched off, without a built-in interval', async () => {
|
||||
const treatmentRule = await rule('TREATMENT_FOLLOW_UP');
|
||||
const feedingRule = await rule('FEEDING_FOLLOW_UP');
|
||||
expect(treatmentRule.active).toBe(false);
|
||||
expect(feedingRule.active).toBe(false);
|
||||
|
||||
const colony = await newColony('OFF');
|
||||
await treat(colony.id);
|
||||
expect(await prisma.task.count({ where: { relatedId: colony.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests a check due the configured days after the treatment ended', async () => {
|
||||
const treatmentRule = await rule('TREATMENT_FOLLOW_UP');
|
||||
await request(server())
|
||||
.patch(`/api/v1/automation-rules/${treatmentRule.id}`)
|
||||
.set(auth())
|
||||
.send({ active: true, config: { dueInDays: 14 } })
|
||||
.expect(200);
|
||||
|
||||
const colony = await newColony('TREAT');
|
||||
await treat(colony.id, {
|
||||
startDate: '2026-08-01T00:00:00.000Z',
|
||||
endDate: '2026-08-10T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP',
|
||||
},
|
||||
});
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].dueDate?.toISOString()).toBe('2026-08-24T00:00:00.000Z');
|
||||
expect(tasks[0].description).toContain('Testmittel');
|
||||
expect(tasks[0].status).toBe('OPEN');
|
||||
|
||||
// A second treatment while the check is still open adds nothing.
|
||||
await treat(colony.id, { date: '2026-08-12T00:00:00.000Z' });
|
||||
expect(
|
||||
await prisma.task.count({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP',
|
||||
},
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('suggests a check after a feeding, counted from the feeding date', async () => {
|
||||
const feedingRule = await rule('FEEDING_FOLLOW_UP');
|
||||
await request(server())
|
||||
.patch(`/api/v1/automation-rules/${feedingRule.id}`)
|
||||
.set(auth())
|
||||
.send({ active: true, config: { dueInDays: 7 } })
|
||||
.expect(200);
|
||||
|
||||
const colony = await newColony('FEED');
|
||||
await request(server())
|
||||
.post('/api/v1/feedings')
|
||||
.set(auth())
|
||||
.send({
|
||||
colonyId: colony.id,
|
||||
date: '2026-09-01T00:00:00.000Z',
|
||||
feedType: 'SUGAR_WATER',
|
||||
amount: 5,
|
||||
unit: 'l',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const task = await prisma.task.findFirstOrThrow({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'FEEDING_FOLLOW_UP',
|
||||
},
|
||||
});
|
||||
expect(task.dueDate?.toISOString()).toBe('2026-09-08T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user