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; 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 { 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 = {}) { 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'); }); });