import type { PermissionResolverService } from '../auth/permissions/permission-resolver.service'; import type { PrismaService } from '../prisma/prisma.service'; import { SuggestionViewsService } from './suggestion-views.service'; const NOW = new Date('2026-05-05T08:00:00.000Z'); function row(id: string, colonyId: string) { return { id, ruleKey: 'INSPECTION_INTERVAL', source: 'RULE', mode: 'DO', intent: 'INSPECT', status: 'OPEN', priority: 'NORMAL', relatedType: 'COLONY', relatedId: colonyId, colonyId, colony: { code: 'B3', apiaryId: 'api_1', apiary: { name: 'Hang' } }, reasonCode: 'UNKNOWN_CODE', evidence: { colonyId }, requiredPermission: 'inspections:read', dueDate: new Date('2026-05-07T00:00:00.000Z'), expiresAt: null, plan: null, decidedAt: null, decisionNote: null, createdAt: NOW, }; } function build(rows: ReturnType[]) { const suggestionFindMany = jest.fn().mockResolvedValue(rows); const colonyFindMany = jest .fn() .mockResolvedValue([{ id: 'col_1', code: 'B3' }]); const prisma = { suggestion: { findMany: suggestionFindMany, groupBy: jest.fn() }, colony: { findMany: colonyFindMany }, queen: { findMany: jest.fn().mockResolvedValue([]) }, treatment: { findMany: jest.fn().mockResolvedValue([]) }, apiary: { findMany: jest.fn().mockResolvedValue([]) }, stockItem: { findMany: jest.fn().mockResolvedValue([]) }, automationRule: { findMany: jest.fn().mockResolvedValue([]) }, $queryRaw: jest.fn().mockResolvedValue([{ cutoff: NOW }]), }; const resolver = { getPermissions: jest.fn().mockResolvedValue(new Set(['inspections:read'])), }; const service = new SuggestionViewsService( prisma as unknown as PrismaService, resolver as unknown as PermissionResolverService, ); return { service, prisma, suggestionFindMany, colonyFindMany }; } describe('SuggestionViewsService', () => { it('filters by requiredPermission in the query, not after loading', async () => { const { service, suggestionFindMany } = build([]); await service.list('user_1', {}); const [args] = suggestionFindMany.mock.calls[0] as [ { where: { AND: unknown[] } }, ]; expect(args.where.AND).toContainEqual({ OR: [ { requiredPermission: null }, { requiredPermission: { in: ['inspections:read'] } }, ], }); expect(service.visibleWhere(new Set(['a:read']))).toEqual({ OR: [ { requiredPermission: null }, { requiredPermission: { in: ['a:read'] } }, ], }); }); it('renders title and reasons with one lookup for all rows', async () => { const { service, colonyFindMany } = build([ row('s1', 'col_1'), row('s2', 'col_1'), row('s3', 'col_1'), ]); const views = await service.list('user_1', {}); expect(views.map((view) => view.id)).toEqual(['s1', 's2', 's3']); expect(views[0]).toMatchObject({ title: 'Durchsicht fällig', colonyCode: 'B3', apiaryId: 'api_1', apiaryName: 'Hang', dueDate: '2026-05-07', cleanup: null, }); expect(colonyFindMany).toHaveBeenCalledTimes(1); }); it('lists a dismissed suggestion only within 30 days of its decision', async () => { const { service, suggestionFindMany, prisma } = build([]); await service.list('user_1', { status: 'DISMISSED' }); expect(prisma.$queryRaw).toHaveBeenCalledTimes(1); const [dismissedArgs] = suggestionFindMany.mock.calls[0] as [ { where: unknown }, ]; expect(JSON.stringify(dismissedArgs.where)).toContain( '"decidedAt":{"gte":"2026-05-05T08:00:00.000Z"}', ); }); it('reads the cleanup tasks from evidence.openTaskIds', async () => { const cleanup = { ...row('s9', 'col_1'), intent: 'CLEANUP', evidence: { colonyId: 'col_1', openTaskIds: ['t1', 't2'], queenId: 'q1' }, }; const { service } = build([cleanup]); const [view] = await service.list('user_1', {}); expect(view.cleanup).toEqual({ taskIds: ['t1', 't2'], queenId: 'q1' }); }); it('passes limit (default 200) and offset to the query', async () => { const { service, suggestionFindMany } = build([]); await service.list('user_1', {}); await service.list('user_1', { limit: 5, offset: 10 }); expect((suggestionFindMany.mock.calls[0] as unknown[])[0]).toMatchObject({ take: 200, skip: 0, }); expect((suggestionFindMany.mock.calls[1] as unknown[])[0]).toMatchObject({ take: 5, skip: 10, }); }); });