Technical Dashboard Aggregate (Task 4.2) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Implement real backend aggregate for GET /dashboard/technical with strict technical ticket scope and operational-only sections.

Architecture: Keep orchestration in TechnicalDashboardService, scope enforcement in DashboardPermissionService, and data aggregation in DashboardQueryService. Reuse the existing dashboard role-service pattern (admin/owner/staff) while extending technical DTOs from placeholders to explicit typed contracts. Ticket scope comes from assignment (assignedTechnicalId) and internal ownership (createdByTechnicalId, INTERNAL_MAINTENANCE) and is applied before building sections.

Tech Stack: NestJS 11, TypeORM 0.3, PostgreSQL enums/entities, Jest, Bun, Biome.


File Structure Map

Modify

  • backend/src/dashboard/dashboard.module.ts
    • Add Ticket to TypeOrmModule.forFeature so dashboard permission/query services can inject ticket repository.
  • backend/src/dashboard/dashboard-permission.service.ts
    • Implement technical ticket scope methods.
  • backend/src/dashboard/dashboard-permission.service.spec.ts
    • Add technical-scope tests.
  • backend/src/dashboard/dashboard-query.service.ts
    • Add technical aggregate builder and section query helpers.
  • backend/src/dashboard/dto/technical-dashboard-response.dto.ts
    • Replace placeholder arrays with explicit item DTOs.
  • backend/src/dashboard/services/technical-dashboard.service.ts
    • Replace placeholder response with real scope + query orchestration.

Create

  • backend/src/dashboard/services/technical-dashboard.service.spec.ts
    • Unit tests for orchestration + sensitive-data boundaries.
  • backend/src/dashboard/dashboard-query.technical.spec.ts
    • Unit tests for technical section aggregation and scope filtering.

Verification Commands

  • bun run backend:test -- src/dashboard/dashboard-permission.service.spec.ts
  • bun run backend:test -- src/dashboard/services/technical-dashboard.service.spec.ts
  • bun run backend:test -- src/dashboard/dashboard-query.technical.spec.ts
  • bun run check

Task 1: Write failing tests for technical scope in permission service (RED)

Files:

  • Modify: backend/src/dashboard/dashboard-permission.service.spec.ts

  • Test: backend/src/dashboard/dashboard-permission.service.spec.ts

  • Step 1: Add failing technical scope tests and repository mock wiring

import { Ticket } from '@/tickets/entities/ticket.entity';
import { TicketType } from '@/tickets/enums/ticket-type.enum';

// In providers array
{
  provide: getRepositoryToken(Ticket),
  useValue: {
    find: jest.fn(),
    exist: jest.fn(),
  },
},

// In beforeEach
let ticketRepository: jest.Mocked<Repository<Ticket>>;
ticketRepository = module.get(getRepositoryToken(Ticket));

it('returns assigned ticket ids for technical user within organization', async () => {
  const technicalUser: CurrentUserData = {
    ...ownerUser,
    userId: 'tech-1',
    role: RoleName.TECHNICAL,
    roles: [RoleName.TECHNICAL],
  };

  ticketRepository.find.mockResolvedValue([
    { id: 'ticket-1' } as Ticket,
    { id: 'ticket-2' } as Ticket,
  ]);

  await expect(service.getTechnicalAssignedTicketIds(technicalUser)).resolves.toEqual([
    'ticket-1',
    'ticket-2',
  ]);
});

it('returns internal ticket ids created by technical user', async () => {
  const technicalUser: CurrentUserData = {
    ...ownerUser,
    userId: 'tech-1',
    role: RoleName.TECHNICAL,
    roles: [RoleName.TECHNICAL],
  };

  ticketRepository.find.mockResolvedValue([{ id: 'ticket-3' } as Ticket]);

  await expect(service.getTechnicalInternalTicketIds(technicalUser)).resolves.toEqual(['ticket-3']);
  expect(ticketRepository.find).toHaveBeenCalledWith(
    expect.objectContaining({
      where: expect.objectContaining({
        createdByTechnicalId: 'tech-1',
        type: TicketType.INTERNAL_MAINTENANCE,
      }),
    }),
  );
});

it('throws when ticket is not assigned to technical user', async () => {
  ticketRepository.exist.mockResolvedValue(false);

  await expect(service.assertTicketAssignedToTechnical('ticket-1', 'tech-1')).rejects.toThrow(
    ForbiddenException,
  );
});

it('throws when internal ticket was not created by technical user', async () => {
  ticketRepository.exist.mockResolvedValue(false);

  await expect(
    service.assertInternalTicketCreatedByTechnical('ticket-2', 'tech-1'),
  ).rejects.toThrow(ForbiddenException);
});
  • Step 2: Run test to verify RED

Run:

bun run backend:test -- src/dashboard/dashboard-permission.service.spec.ts

Expected: FAIL with missing method errors (getTechnicalInternalTicketIds) and/or constructor injection mismatch for Ticket repo.

  • Step 3: Commit failing tests
git add backend/src/dashboard/dashboard-permission.service.spec.ts
git commit -m "test: define technical ticket scope behavior in dashboard permission service"

Task 2: Implement permission scope for technical tickets (GREEN)

Files:

  • Modify: backend/src/dashboard/dashboard.module.ts

  • Modify: backend/src/dashboard/dashboard-permission.service.ts

  • Test: backend/src/dashboard/dashboard-permission.service.spec.ts

  • Step 1: Register Ticket entity in dashboard module

import { Ticket } from '@/tickets/entities/ticket.entity';

TypeOrmModule.forFeature([
  Area,
  Pond,
  Device,
  DeviceAlert,
  DailyLog,
  GrowthRecord,
  FarmingCycle,
  User,
  Organization,
  Ticket,
]);
  • Step 2: Implement technical scope methods in permission service
import { Ticket } from '@/tickets/entities/ticket.entity';
import { TicketType } from '@/tickets/enums/ticket-type.enum';

constructor(
  @InjectRepository(Area)
  private areaRepository: Repository<Area>,
  @InjectRepository(Pond)
  private pondRepository: Repository<Pond>,
  @InjectRepository(Ticket)
  private ticketRepository: Repository<Ticket>,
) {}

async getTechnicalAssignedTicketIds(currentUser: CurrentUserData): Promise<string[]> {
  const organizationId = this.getOrganizationIdOrThrow(currentUser);
  const tickets = await this.ticketRepository.find({
    where: {
      organizationId,
      assignedTechnicalId: currentUser.userId,
    },
    select: { id: true },
  });
  return tickets.map((ticket) => ticket.id);
}

async getTechnicalInternalTicketIds(currentUser: CurrentUserData): Promise<string[]> {
  const organizationId = this.getOrganizationIdOrThrow(currentUser);
  const tickets = await this.ticketRepository.find({
    where: {
      organizationId,
      createdByTechnicalId: currentUser.userId,
      type: TicketType.INTERNAL_MAINTENANCE,
    },
    select: { id: true },
  });
  return tickets.map((ticket) => ticket.id);
}

async assertTicketAssignedToTechnical(ticketId: string, userId: string): Promise<void> {
  const assigned = await this.ticketRepository.exist({
    where: { id: ticketId, assignedTechnicalId: userId },
  });
  if (!assigned) {
    throw new ForbiddenException('Ticket is not assigned to this technical user');
  }
}

async assertInternalTicketCreatedByTechnical(ticketId: string, userId: string): Promise<void> {
  const owned = await this.ticketRepository.exist({
    where: {
      id: ticketId,
      createdByTechnicalId: userId,
      type: TicketType.INTERNAL_MAINTENANCE,
    },
  });
  if (!owned) {
    throw new ForbiddenException('Internal ticket is not created by this technical user');
  }
}
  • Step 3: Run test to verify GREEN

Run:

bun run backend:test -- src/dashboard/dashboard-permission.service.spec.ts

Expected: PASS for all existing + new permission scope tests.

  • Step 4: Commit permission implementation
git add backend/src/dashboard/dashboard.module.ts backend/src/dashboard/dashboard-permission.service.ts
git commit -m "feat: implement technical ticket scope in dashboard permission service"

Task 3: Write failing tests for technical dashboard orchestration service (RED)

Files:

  • Create: backend/src/dashboard/services/technical-dashboard.service.spec.ts

  • Test: backend/src/dashboard/services/technical-dashboard.service.spec.ts

  • Step 1: Create failing service tests

import { ForbiddenException } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import type { CurrentUserData } from '@/auth/decorators/current-user.decorator';
import { RoleName } from '@/common/enums';
import { DashboardPermissionService } from '@/dashboard/dashboard-permission.service';
import { DashboardQueryService } from '@/dashboard/dashboard-query.service';
import { TechnicalDashboardService } from '@/dashboard/services/technical-dashboard.service';

describe('TechnicalDashboardService', () => {
  let service: TechnicalDashboardService;
  let dashboardPermissionService: jest.Mocked<DashboardPermissionService>;
  let dashboardQueryService: jest.Mocked<DashboardQueryService>;

  const technicalUser: CurrentUserData = {
    userId: 'tech-1',
    email: 'tech@example.com',
    organizationId: 'org-1',
    role: RoleName.TECHNICAL,
    roles: [RoleName.TECHNICAL],
    permissions: [],
    assignedResources: [],
    assignedAreaIds: [],
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TechnicalDashboardService,
        {
          provide: DashboardPermissionService,
          useValue: {
            assertRole: jest.fn(),
            getOrganizationIdOrThrow: jest.fn().mockReturnValue('org-1'),
            getTechnicalAssignedTicketIds: jest.fn().mockResolvedValue(['ticket-a']),
            getTechnicalInternalTicketIds: jest.fn().mockResolvedValue(['ticket-b']),
          },
        },
        {
          provide: DashboardQueryService,
          useValue: {
            buildGeneratedAtScope: jest.fn().mockReturnValue({
              generatedAt: '2026-04-26T00:00:00.000Z',
              scope: 'own',
            }),
            buildTechnicalSections: jest.fn().mockResolvedValue({
              assignedTicketQueue: [{ id: 'ticket-a' }],
              internalTicketQueue: [{ id: 'ticket-b' }],
              todayRoute: [],
              deviceDiagnostics: [],
              partsNeeded: [],
              maintenanceHistory: [],
              serviceReportState: { pending: 1, completed: 0 },
            }),
          },
        },
      ],
    }).compile();

    service = module.get(TechnicalDashboardService);
    dashboardPermissionService = module.get(DashboardPermissionService);
    dashboardQueryService = module.get(DashboardQueryService);
  });

  it('builds technical dashboard using assigned/internal ticket scopes', async () => {
    const result = await service.getDashboard(technicalUser);

    expect(dashboardQueryService.buildTechnicalSections).toHaveBeenCalledWith({
      organizationId: 'org-1',
      userId: 'tech-1',
      assignedTicketIds: ['ticket-a'],
      internalTicketIds: ['ticket-b'],
    });

    expect(result.scope).toBe('own');
    expect(result.kpis.total).toBe(2);
  });

  it('does not expose business-sensitive dashboard metrics', async () => {
    const result = await service.getDashboard(technicalUser);
    expect((result.sections as Record<string, unknown>).cost).toBeUndefined();
    expect((result.sections as Record<string, unknown>).revenue).toBeUndefined();
    expect((result.sections as Record<string, unknown>).fcr).toBeUndefined();
  });

  it('propagates forbidden error for non-technical role', async () => {
    dashboardPermissionService.assertRole.mockImplementation(() => {
      throw new ForbiddenException('Role is not allowed');
    });

    await expect(service.getDashboard(technicalUser)).rejects.toThrow(ForbiddenException);
  });
});
  • Step 2: Run test to verify RED

Run:

bun run backend:test -- src/dashboard/services/technical-dashboard.service.spec.ts

Expected: FAIL because buildTechnicalSections and internal ticket scope call flow are not implemented yet.

  • Step 3: Commit failing tests
git add backend/src/dashboard/services/technical-dashboard.service.spec.ts
git commit -m "test: define technical dashboard aggregation orchestration"

Task 4: Write failing tests for technical query aggregation (RED)

Files:

  • Create: backend/src/dashboard/dashboard-query.technical.spec.ts

  • Test: backend/src/dashboard/dashboard-query.technical.spec.ts

  • Step 1: Create failing query-level tests for scope filtering

import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DashboardQueryService } from '@/dashboard/dashboard-query.service';
import { Ticket } from '@/tickets/entities/ticket.entity';
import { TicketPriority } from '@/tickets/enums/ticket-priority.enum';
import { TicketStatus } from '@/tickets/enums/ticket-status.enum';
import { TicketType } from '@/tickets/enums/ticket-type.enum';

describe('DashboardQueryService technical sections', () => {
  let service: DashboardQueryService;
  const ticketRepository = { find: jest.fn() };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        DashboardQueryService,
        { provide: getRepositoryToken(Ticket), useValue: ticketRepository },
        // Other repositories injected by DashboardQueryService can be minimal stubs
        { provide: getRepositoryToken(require('@/farm/entities/area.entity').Area), useValue: {} },
        { provide: getRepositoryToken(require('@/farm/entities/pond.entity').Pond), useValue: {} },
        { provide: getRepositoryToken(require('@/devices/entities/device.entity').Device), useValue: {} },
        { provide: getRepositoryToken(require('@/devices/entities/device-alert.entity').DeviceAlert), useValue: {} },
        { provide: getRepositoryToken(require('@/aquaculture/entities/daily-log.entity').DailyLog), useValue: {} },
        { provide: getRepositoryToken(require('@/aquaculture/entities/growth-record.entity').GrowthRecord), useValue: {} },
        { provide: getRepositoryToken(require('@/aquaculture/entities/farming-cycle.entity').FarmingCycle), useValue: {} },
        { provide: getRepositoryToken(require('@/auth/entities/user.entity').User), useValue: {} },
        { provide: getRepositoryToken(require('@/organizations/entities/organization.entity').Organization), useValue: {} },
      ],
    }).compile();

    service = module.get(DashboardQueryService);
  });

  it('builds sections from assigned/internal scope only', async () => {
    ticketRepository.find.mockResolvedValue([
      {
        id: 'ticket-a',
        type: TicketType.CUSTOMER,
        status: TicketStatus.ASSIGNED,
        priority: TicketPriority.HIGH,
        title: 'Ticket A',
        pondId: 'pond-1',
        deviceId: 'device-1',
        farm: { id: 'farm-1', name: 'Farm 1' },
        pond: { id: 'pond-1', name: 'Pond 1' },
        device: { id: 'device-1', name: 'Device 1', status: 'offline', lastSeenAt: null },
        updatedAt: new Date('2026-04-26T08:00:00.000Z'),
      } as Ticket,
    ]);

    const sections = await service.buildTechnicalSections({
      organizationId: 'org-1',
      userId: 'tech-1',
      assignedTicketIds: ['ticket-a'],
      internalTicketIds: [],
    });

    expect(sections.assignedTicketQueue).toHaveLength(1);
    expect(sections.todayRoute).toHaveLength(1);
    expect(sections.deviceDiagnostics).toHaveLength(1);
    expect(sections.partsNeeded).toHaveLength(0);
  });
});
  • Step 2: Run test to verify RED

Run:

bun run backend:test -- src/dashboard/dashboard-query.technical.spec.ts

Expected: FAIL because buildTechnicalSections does not exist in query service.

  • Step 3: Commit failing query tests
git add backend/src/dashboard/dashboard-query.technical.spec.ts
git commit -m "test: define technical dashboard query aggregation boundaries"

Task 5: Implement technical DTO, query aggregation, and service orchestration (GREEN)

Files:

  • Modify: backend/src/dashboard/dto/technical-dashboard-response.dto.ts

  • Modify: backend/src/dashboard/dashboard-query.service.ts

  • Modify: backend/src/dashboard/services/technical-dashboard.service.ts

  • Test: backend/src/dashboard/services/technical-dashboard.service.spec.ts

  • Test: backend/src/dashboard/dashboard-query.technical.spec.ts

  • Step 1: Replace technical DTO placeholders with explicit section contracts

import type { DashboardBaseResponseDto } from '@/dashboard/dto/dashboard-base-response.dto';

export interface TechnicalTicketQueueItemDto {
  id: string;
  title: string;
  type: 'customer' | 'internal_maintenance';
  status: string;
  priority: string;
  farmId: string | null;
  farmName: string | null;
  pondId: string | null;
  pondName: string | null;
  deviceId: string | null;
  deviceName: string | null;
  updatedAt: string;
}

export interface TechnicalTodayRouteItemDto {
  ticketId: string;
  priority: string;
  farmName: string | null;
  pondName: string | null;
  locationLabel: string;
  status: string;
}

export interface TechnicalDeviceDiagnosticsItemDto {
  ticketId: string;
  deviceId: string;
  deviceName: string;
  deviceStatus: string;
  lastSeenAt: string | null;
}

export interface TechnicalPartNeededItemDto {
  ticketId: string;
  title: string;
  priority: string;
  status: string;
}

export interface TechnicalMaintenanceHistoryItemDto {
  ticketId: string;
  title: string;
  status: string;
  completedAt: string;
}

export interface TechnicalServiceReportStateDto {
  pending: number;
  inProgress: number;
  completed: number;
  waitingParts: number;
}

export interface TechnicalDashboardSectionsDto {
  assignedTicketQueue: TechnicalTicketQueueItemDto[];
  internalTicketQueue: TechnicalTicketQueueItemDto[];
  todayRoute: TechnicalTodayRouteItemDto[];
  deviceDiagnostics: TechnicalDeviceDiagnosticsItemDto[];
  partsNeeded: TechnicalPartNeededItemDto[];
  maintenanceHistory: TechnicalMaintenanceHistoryItemDto[];
  serviceReportState: TechnicalServiceReportStateDto;
}

export type TechnicalDashboardResponseDto = DashboardBaseResponseDto<TechnicalDashboardSectionsDto>;
  • Step 2: Add technical section builder in query service with strict ticket scope filter
import { In } from 'typeorm';
import { Ticket } from '@/tickets/entities/ticket.entity';
import { TicketStatus } from '@/tickets/enums/ticket-status.enum';

@InjectRepository(Ticket)
private ticketRepository: Repository<Ticket>,

async buildTechnicalSections(params: {
  organizationId: string;
  userId: string;
  assignedTicketIds: string[];
  internalTicketIds: string[];
}): Promise<TechnicalDashboardSectionsDto> {
  const scopedTicketIds = Array.from(
    new Set([...params.assignedTicketIds, ...params.internalTicketIds]),
  );

  if (scopedTicketIds.length === 0) {
    return this.buildEmptyTechnicalSections();
  }

  const tickets = await this.ticketRepository.find({
    where: {
      organizationId: params.organizationId,
      id: In(scopedTicketIds),
    },
    relations: {
      farm: true,
      pond: true,
      device: true,
    },
    order: { updatedAt: 'DESC' },
    take: 100,
  });

  const assignedSet = new Set(params.assignedTicketIds);
  const internalSet = new Set(params.internalTicketIds);

  const assignedTicketQueue = tickets
    .filter((ticket) => assignedSet.has(ticket.id))
    .map((ticket) => this.toTechnicalQueueItem(ticket));

  const internalTicketQueue = tickets
    .filter((ticket) => internalSet.has(ticket.id))
    .map((ticket) => this.toTechnicalQueueItem(ticket));

  const todayRoute = tickets
    .filter((ticket) => assignedSet.has(ticket.id))
    .filter((ticket) =>
      [
        TicketStatus.ASSIGNED,
        TicketStatus.ACCEPTED,
        TicketStatus.ON_THE_WAY,
        TicketStatus.IN_PROGRESS,
        TicketStatus.WAITING_PARTS,
        TicketStatus.WAITING_CUSTOMER,
      ].includes(ticket.status),
    )
    .slice(0, 20)
    .map((ticket) => ({
      ticketId: ticket.id,
      priority: ticket.priority,
      farmName: ticket.farm?.name ?? null,
      pondName: ticket.pond?.name ?? null,
      locationLabel: ticket.pond?.name ?? ticket.farm?.name ?? 'Điểm chưa xác định',
      status: ticket.status,
    }));

  const deviceDiagnostics = tickets
    .filter((ticket) => Boolean(ticket.device?.id))
    .map((ticket) => ({
      ticketId: ticket.id,
      deviceId: ticket.device?.id ?? '',
      deviceName: ticket.device?.name ?? 'Thiết bị không tên',
      deviceStatus: ticket.device?.status ?? 'unknown',
      lastSeenAt: ticket.device?.lastSeenAt ? new Date(ticket.device.lastSeenAt).toISOString() : null,
    }));

  const partsNeeded = tickets
    .filter((ticket) => ticket.status === TicketStatus.WAITING_PARTS)
    .map((ticket) => ({
      ticketId: ticket.id,
      title: ticket.title,
      priority: ticket.priority,
      status: ticket.status,
    }));

  const maintenanceHistory = tickets
    .filter((ticket) => internalSet.has(ticket.id))
    .filter((ticket) => [TicketStatus.RESOLVED, TicketStatus.VERIFIED, TicketStatus.CLOSED].includes(ticket.status))
    .slice(0, 20)
    .map((ticket) => ({
      ticketId: ticket.id,
      title: ticket.title,
      status: ticket.status,
      completedAt: ticket.updatedAt.toISOString(),
    }));

  const serviceReportState = {
    pending: tickets.filter((ticket) => ticket.status === TicketStatus.ASSIGNED).length,
    inProgress: tickets.filter((ticket) => ticket.status === TicketStatus.IN_PROGRESS).length,
    completed: tickets.filter((ticket) => ticket.status === TicketStatus.RESOLVED).length,
    waitingParts: tickets.filter((ticket) => ticket.status === TicketStatus.WAITING_PARTS).length,
  };

  return {
    assignedTicketQueue,
    internalTicketQueue,
    todayRoute,
    deviceDiagnostics,
    partsNeeded,
    maintenanceHistory,
    serviceReportState,
  };
}

private toTechnicalQueueItem(ticket: Ticket): TechnicalTicketQueueItemDto {
  return {
    id: ticket.id,
    title: ticket.title,
    type: ticket.type,
    status: ticket.status,
    priority: ticket.priority,
    farmId: ticket.farmId,
    farmName: ticket.farm?.name ?? null,
    pondId: ticket.pondId,
    pondName: ticket.pond?.name ?? null,
    deviceId: ticket.deviceId,
    deviceName: ticket.device?.name ?? null,
    updatedAt: ticket.updatedAt.toISOString(),
  };
}
  • Step 3: Replace technical dashboard service placeholder orchestration
async getDashboard(currentUser: CurrentUserData): Promise<TechnicalDashboardResponseDto> {
  this.dashboardPermissionService.assertRole(currentUser, [RoleName.TECHNICAL]);

  const organizationId = this.dashboardPermissionService.getOrganizationIdOrThrow(currentUser);
  const [assignedTicketIds, internalTicketIds] = await Promise.all([
    this.dashboardPermissionService.getTechnicalAssignedTicketIds(currentUser),
    this.dashboardPermissionService.getTechnicalInternalTicketIds(currentUser),
  ]);

  const sections = await this.dashboardQueryService.buildTechnicalSections({
    organizationId,
    userId: currentUser.userId,
    assignedTicketIds,
    internalTicketIds,
  });

  const base = this.dashboardQueryService.buildGeneratedAtScope('own');
  const uniqueScopeTicketCount = new Set([...assignedTicketIds, ...internalTicketIds]).size;

  return {
    generatedAt: base.generatedAt,
    scope: 'own',
    kpis: { total: uniqueScopeTicketCount },
    sections,
  };
}
  • Step 4: Run tests to verify GREEN

Run:

bun run backend:test -- src/dashboard/services/technical-dashboard.service.spec.ts
bun run backend:test -- src/dashboard/dashboard-query.technical.spec.ts
bun run backend:test -- src/dashboard/dashboard-permission.service.spec.ts

Expected: PASS, including scope-specific technical tests.

  • Step 5: Commit technical aggregate implementation
git add \
  backend/src/dashboard/dto/technical-dashboard-response.dto.ts \
  backend/src/dashboard/dashboard-query.service.ts \
  backend/src/dashboard/services/technical-dashboard.service.ts
git commit -m "feat: implement technical dashboard aggregate with scoped ticket sections"

Task 6: Full verification and final hardening checks

Files:

  • Modify (if needed): files failing lint/tests from Task 1-5

  • Test: all files in this plan

  • Step 1: Run full required verification commands

Run:

bun run backend:test -- src/dashboard/dashboard-permission.service.spec.ts
bun run backend:test -- src/dashboard/services/technical-dashboard.service.spec.ts
bun run backend:test -- src/dashboard/dashboard-query.technical.spec.ts
bun run check

Expected:

  • All targeted tests PASS.

  • bun run check exits 0.

  • Step 2: Manual acceptance checklist against Task 4.2 spec

  • Assigned ticket queue returns only assignedTechnicalId = currentUser.userId scope.

  • Internal ticket queue returns only createdByTechnicalId = currentUser.userId + INTERNAL_MAINTENANCE.

  • Today route and device diagnostics are derived only from scoped ticket IDs.

  • No cost/revenue/FCR/survival/staff performance/daily-log detail in technical response DTO.

  • Cross-technical ticket access checks throw ForbiddenException in permission service assertions.

  • Step 3: Commit final polish (if any fixes were needed)

git add backend/src/dashboard
git commit -m "test: verify technical dashboard role isolation and acceptance criteria"

Spec Coverage Self-Check

  • Scope isolation for technical assigned/internal tickets: covered in Tasks 1, 2, 3, 4, 5, 6.
  • Technical sections implementation: covered in Task 5.
  • Forbidden cross-technical behavior: covered in Tasks 1 and 6.
  • Sensitive metric exclusion: covered in Tasks 3, 5, and 6.
  • Verification evidence (bun run check + tests): covered in Task 6.

No uncovered spec requirements remain.